From 1360e73f11914849ea69adfcd5d022d3aa912844 Mon Sep 17 00:00:00 2001 From: ewsgit Date: Sun, 2 Nov 2025 20:49:54 +0000 Subject: [PATCH 1/3] changes --- README.md | 48 +- package.json | 2 +- .../uk.tcsw.dashboard/manifest.json | 2 +- packages/instance/.temp/log.ts | 2 +- packages/instance/.temp/requestManager.ts | 1351 ++++++++--------- packages/instance/src/index.ts | 27 +- packages/instance/src/log.ts | 38 +- packages/instance/src/subSystems.ts | 5 +- .../instance/src/subsystems/authorization.ts | 21 +- .../instance/src/subsystems/configuration.ts | 2 +- .../src/subsystems/consoleCommands.ts | 10 +- .../consoleCommands/commands/password.ts | 3 +- .../consoleCommands/commands/restart.ts | 39 +- packages/instance/src/subsystems/database.ts | 6 + .../instance/src/subsystems/notifications.ts | 17 +- packages/instance/src/subsystems/trpc.ts | 5 +- packages/instance/src/subsystems/users.ts | 173 ++- packages/uikit-solid/package.json | 2 +- 18 files changed, 880 insertions(+), 873 deletions(-) diff --git a/README.md b/README.md index 6282451..32bddc5 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,7 @@ A working environment for professionals and Home-Lab users. ### Links - Discord -> https://discord.gg/jcJeGEAYhY -- Git -> https://git.tcsw.uk/tricolor/workspaces -- Git (Mirror) -> https://github.com/tcswuk/workspaces - -> [!IMPORTANT] -> Pull requests and issues not reported on https://git.tcsw.uk/tricolor/workspaces will be ignored. +- Source Code (git) -> https://github.com/tricolorsoftware/workspaces ## Installation Guide for Production Environments --- @@ -25,32 +21,29 @@ A working environment for professionals and Home-Lab users. 3. `cd /` 4. create a directory to house the Workspaces filesystem root e.g.: `sudo mkdir /tricolor/workspaces` 5. change into the newly created directory `cd /tricolor/workspaces` -6. clone the workspaces docker configuration from git `git clone ssh://git@git.tcsw.uk:9000/tricolor/workspaces-docker.git .` +6. clone the workspaces docker configuration from git `git clone git@github.com:tricolorsoftware/workspaces-docker.git .` 7. edit `docker-compose.yaml` to your desired configuration from [Insert link to relevant documentation here] before continuing. 8. run `docker-compose up` to start the workspaces docker image and install all remaining dependencies 9. open your browser and head to `https://[server-ip]` and follow the on-screen instructions to complete the setup. 10. Enjoy! :D - - ## Installation Guide for Development Environments --- > [!WARNING] -> Ignore this section if you are using the pre-built docker container image, this is only for local development outside of a docker environment +> Ignore this section if you are planning to use workspaces in production, instead please follow the `Installation Guide for Production Environments` section. ### Dependencies -| Dependency | NPM Package | External Installation Guide | Optional | | -| --------------- | ------------------------- | --------------------------- | -------- | --- | -| SolidJS | solid | | No | | -| Vite | vite | | No | | -| UIKit | @tcsw/uikit-mv3-solid | | No | | -| Bun | | https://bun.sh | No | | -| Docker | | https://www.docker.com/ | Yes | | -| PostgreSQL | | https://www.postgresql.org/ | No | | +| Dependency | NPM Package | External Installation Guide | Optional | +| --------------- | ------------------------- | --------------------------- | -------- | +| SolidJS | solid | | No | +| Vite | vite | | No | +| UIKit | @tcsw/uikit-solid | | No | +| Bun | | https://bun.sh | No | +| Docker | | https://www.docker.com/ | Yes | +| PostgreSQL | | https://www.postgresql.org/ | No | ### Installation -1. Ensure all submodules are cloned/up-to-date with `git submodule update --init -2. Choose either the `Full-Stack` or `UIKit` section to continue +1. Choose either the `Full-Stack` or `UIKit` section to continue #### Full-Stack (Workspaces) 1. Ensure all non-NPM dependencies are installed before continuing 2. Run `bun install` to install all NPM dependencies @@ -59,7 +52,7 @@ A working environment for professionals and Home-Lab users. #### UIKit (UI Library dev page) 1. Ensure all non-NPM dependencies are installed before continuing 2. Run `bun install` to install all NPM dependencies -3. cd into the UIKit directory `cd ./packages/uikit-mv3-solid` +3. cd into the UIKit directory `cd ./packages/uikit-solid` 4. Run `bun run dev-web` to start up the web interface and backend in development mode 5. Enjoy! :D @@ -75,18 +68,3 @@ A working environment for professionals and Home-Lab users. - login / signup flow - Application-Frame layout - Notification visuals - - -How do applications work? - -Backend starts up -Backend verifies the filesystem -Backend creates a Applications.tsx file in /fs -Web starts up -Web uses /fs/Applications.tsx as the router for /app - -Applications can now be accessed by /app/[application-id]/ - -Applications can be external or located in /fs/applications/ -Only applications specified in /fs/applications.json as enabled will be loaded -Any number of unloaded applications can also be specified in /fs/applications.json and they will be enablable by the administrator diff --git a/package.json b/package.json index eb2ad78..cbf5f2b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ ], "repository": { "type": "git", - "url": "https://git.tcsw.uk/tricolor/workspaces" + "url": "https://github.com/tricolorsoftware/workspaces" }, "scripts": { "web": "bun run --cwd packages/instance dev-web", diff --git a/packages/applications/uk.tcsw.dashboard/manifest.json b/packages/applications/uk.tcsw.dashboard/manifest.json index 919baa9..cb5d956 100644 --- a/packages/applications/uk.tcsw.dashboard/manifest.json +++ b/packages/applications/uk.tcsw.dashboard/manifest.json @@ -4,7 +4,7 @@ "description": "The Homepage for workspaces", "authors": ["Tricolor Software "], "license": "MIT", - "source": "https://git.tcsw.uk/tricolor/workspaces/src/branch/main/packages/applications/uk.tcsw.dashboard", + "source": "https://github.com/tricolorsoftware/workspaces/src/branch/main/packages/applications/uk.tcsw.dashboard", "modules": { "web": { "path": "./web" }, "bun": { "path": "./backend/index.ts" } diff --git a/packages/instance/.temp/log.ts b/packages/instance/.temp/log.ts index 8e79647..6a8d2e6 100644 --- a/packages/instance/.temp/log.ts +++ b/packages/instance/.temp/log.ts @@ -137,7 +137,7 @@ export default class Log { this._internal_cursorTo(this.instance.log.metaLength + 6, this._internal_getWindowSize()[1] - 2, () => { if (this.instance.configurationManager?.hasFeature(YourDashFeatureFlags.SlashCommands)) { // write the prompt indicator to the stdout - process.stdout.write(`> ${this.instance.commandManager?.rlInterface?.line}`); + process.stdout.write(`> ${this.instance.commandManager?.rlInterface?.line || "unknown value"}`); } else { process.stdout.write(" "); } diff --git a/packages/instance/.temp/requestManager.ts b/packages/instance/.temp/requestManager.ts index 2174c53..3fe606d 100644 --- a/packages/instance/.temp/requestManager.ts +++ b/packages/instance/.temp/requestManager.ts @@ -39,12 +39,12 @@ class RequestManager { this.tRPC = initTRPC.create(); this.tRPCRouter = this.tRPC.router({ - sample: this.tRPC.procedure.query(async (options) => { - return { - foo: "bar" - }; - }) - }); + sample: this.tRPC.procedure.query(async (options) => { + return { + foo: "bar", + }; + }), + }); return this; } @@ -64,173 +64,209 @@ class RequestManager { console.error(err); } - this.app.use(cors({ - methods: "*", - origin: "http://localhost:5173", - credentials: true - })); + this.app.use( + cors({ + methods: "*", + origin: "http://localhost:5173", + credentials: true, + }), + ); - this.app.use(cookies.express([ this.instance.configurationManager.config.cookieSecret ])); + this.app.use(cookies.express([this.instance.configurationManager.config.cookieSecret])); this.app.use( "/trpc", trpcExpress.createExpressMiddleware({ - router: this.tRPCRouter, - createContext, - onError({ path, error }) { - console.error( - `Error in tRPC handler on path '${path}':`, error); - } - }) + router: this.tRPCRouter, + createContext, + onError({ path, error }) { + console.error(`Error in tRPC handler on path '${path}':`, error); + }, + }), ); this.app.use((req, res, next) => { - req.on("finish", async (req, res) => { - if (res.statusCode.toString()[0] === "4") { - switch (req.method.toUpperCase()) { - case "GET": - if (req.url.startsWith("/core/auth-img")) { - return; - } - if (req.url.startsWith("/core/auth-video")) { - return; - } - - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.green(" GET "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - case "POST": - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.blue(" POS "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - case "DELETE": - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.red(" DEL "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - case "OPTIONS": - if (this.instance.configurationManager.config.logOptionsRequests) { - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.cyan(" OPT "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - } - break; - case "PROPFIND": - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.cyan(" PFI "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - case "PROPPATCH": - this.instance.log.info( - "response", - `${chalk.bgCyan(chalk.cyan(" PPA "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - default: - } - } - if (res.statusCode.toString()[0] === "5") { - switch (req.method.toUpperCase()) { - case "GET": - if (req.url.startsWith("/core/auth-img")) { - return; - } - if (req.url.startsWith("/core/auth-video")) { - return; - } - - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.green(" GET "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - case "POST": - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.blue(" POS "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - case "DELETE": - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.red(" DEL "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - case "OPTIONS": - if (this.instance.configurationManager.config.logOptionsRequests) { - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.cyan(" OPT "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - } - break; - case "PROPFIND": - this.instance.log.info( - "response", - `${chalk.bgBlack(chalk.cyan(" PFI "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - case "PROPPATCH": - this.instance.log.info( - "response", - `${chalk.bgCyan(chalk.cyan(" PPA "))} ${chalk.bgBlack( - chalk.red(` ${res.statusCode} `))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` - ); - break; - default: - } - } - }); - - return next(); - } - ); + req.on("finish", async (req, res) => { + if (res.statusCode.toString()[0] === "4") { + switch (req.method.toUpperCase()) { + case "GET": + if (req.url.startsWith("/core/auth-img")) { + return; + } + if (req.url.startsWith("/core/auth-video")) { + return; + } + + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.green(" GET "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + case "POST": + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.blue(" POS "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + case "DELETE": + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.red(" DEL "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + case "OPTIONS": + if (this.instance.configurationManager.config.logOptionsRequests) { + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.cyan(" OPT "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + } + break; + case "PROPFIND": + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.cyan(" PFI "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + case "PROPPATCH": + this.instance.log.info( + "response", + `${chalk.bgCyan(chalk.cyan(" PPA "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bgBlack( + chalk.red(` ${res.statusCode} `), + )} ${chalk.bold(req.url)} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + default: + } + } + if (res.statusCode.toString()[0] === "5") { + switch (req.method.toUpperCase()) { + case "GET": + if (req.url.startsWith("/core/auth-img")) { + return; + } + if (req.url.startsWith("/core/auth-video")) { + return; + } + + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.green(" GET "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + case "POST": + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.blue(" POS "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + case "DELETE": + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.red(" DEL "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + case "OPTIONS": + if (this.instance.configurationManager.config.logOptionsRequests) { + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.cyan(" OPT "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + } + break; + case "PROPFIND": + this.instance.log.info( + "response", + `${chalk.bgBlack(chalk.cyan(" PFI "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + case "PROPPATCH": + this.instance.log.info( + "response", + `${chalk.bgCyan(chalk.cyan(" PPA "))} ${chalk.bgBlack(chalk.red(` ${res.statusCode} `))} ${chalk.bold( + req.url, + )} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, + ); + break; + default: + } + } + }); + + return next(); + }); this.app.use(async (req, res, next) => { - let queryResult = await this.instance.database`INSERT INTO public.request_manager_log (request_timestamp, request_method, request_path, request_body) + let queryResult = await this.instance + .database`INSERT INTO public.request_manager_log (request_timestamp, request_method, request_path, request_body) VALUES (${Date.now()}, ${req.method}, ${req.url}, ${req.body}) RETURNING request_id;`; @@ -248,59 +284,73 @@ class RequestManager { this.instance.log.info( "request", - `${chalk.bgBlack(chalk.green(" GET "))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` + `${chalk.bgBlack(chalk.green(" GET "))} ${chalk.bold(req.url)} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, ); break; case "POST": this.instance.log.info( "request", - `${chalk.bgBlack(chalk.blue(" POS "))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` + `${chalk.bgBlack(chalk.blue(" POS "))} ${chalk.bold(req.url)} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, ); break; case "DELETE": this.instance.log.info( "request", - `${chalk.bgBlack(chalk.red(" DEL "))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` + `${chalk.bgBlack(chalk.red(" DEL "))} ${chalk.bold(req.url)} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, ); break; case "OPTIONS": if (this.instance.configurationManager.config.logOptionsRequests) { this.instance.log.info( "request", - `${chalk.bgBlack(chalk.cyan(" OPT "))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` + `${chalk.bgBlack(chalk.cyan(" OPT "))} ${chalk.bold(req.url)} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, ); } break; case "PROPFIND": this.instance.log.info( "request", - `${chalk.bgBlack(chalk.cyan(" PFI "))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` + `${chalk.bgBlack(chalk.cyan(" PFI "))} ${chalk.bold(req.url)} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, ); break; case "PROPPATCH": this.instance.log.info( "request", - `${chalk.bgCyan(chalk.cyan(" PPA "))} ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` + `${chalk.bgCyan(chalk.cyan(" PPA "))} ${chalk.bold(req.url)} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, ); break; default: this.instance.log.error( "core_requests", - `ERROR IN REQUEST LOGGER, UNKNOWN REQUEST TYPE: ${req.method}, ${chalk.bold( - req.url)} ${this.instance.configurationManager.config.logQueryParameters ? JSON.stringify( - req.query) !== "{}" && JSON.stringify(req.query) : ""}` + `ERROR IN REQUEST LOGGER, UNKNOWN REQUEST TYPE: ${req.method}, ${chalk.bold(req.url)} ${ + this.instance.configurationManager.config.logQueryParameters + ? JSON.stringify(req.query) !== "{}" && JSON.stringify(req.query) + : "" + }`, ); } @@ -311,503 +361,365 @@ class RequestManager { const self = this; this.instance.authorization.publicPaths.push("/"); - this.app.get( - "/", - async function handler(req, res) { - if (self.instance.configurationManager.config.isDevMode) { - return res.redirect( - `http://localhost:5173/login/http://localhost:3563` - ); - } - - return res.redirect( - `https://yourdash.ewsgit.uk/login/${/* this.globalDb.get("core:this.instanceurl") */ "FIXME: implement this"}` - ); + this.app.get("/", async function handler(req, res) { + if (self.instance.configurationManager.config.isDevMode) { + return res.redirect(`http://localhost:5173/login/http://localhost:3563`); } - ); + + return res.redirect( + `https://yourdash.ewsgit.uk/login/${/* this.globalDb.get("core:this.instanceurl") */ "FIXME: implement this"}`, + ); + }); this.instance.authorization.publicPaths.push("/test"); - this.app.get( - "/test", - async () => { - return { - version: { major: 0, minor: 1 }, - type: "YourDash" as const, - status: this.instance.getStatus() || InstanceStatus.NON_FUNCTIONAL - }; - } - ); + this.app.get("/test", async () => { + return { + version: { major: 0, minor: 1 }, + type: "YourDash" as const, + status: this.instance.getStatus() || InstanceStatus.NON_FUNCTIONAL, + }; + }); this.instance.authorization.publicPaths.push("/418"); - this.app.get( - "/418", - async () => { - return "This is a yourdash instance, not a coffee pot. This server does not implement the Hyper Text Coffee Pot Control Protocol"; - } - ); + this.app.get("/418", async () => { + return "This is a yourdash instance, not a coffee pot. This server does not implement the Hyper Text Coffee Pot Control Protocol"; + }); this.instance.authorization.publicPaths.push("/ping"); - this.app.get( - "/ping", - async () => { - return "pong"; - } - ); + this.app.get("/ping", async () => { + return "pong"; + }); this.instance.authorization.publicPaths.push("/core/test/self-ping"); - this.app.get( - "/core/test/self-ping", - async (req, res) => { - return res.json({ success: true }); - } - ); + this.app.get("/core/test/self-ping", async (req, res) => { + return res.json({ success: true }); + }); this.instance.authorization.publicPaths.push("/login/instance/metadata"); - this.app.get( - "/login/instance/metadata", - (req, res) => { - return res.json({ - title: "YourDash Instance", - message: - "Placeholder message. Hey system admin, you should change this!", - loginLayout: LoginLayout.CARDS - }); - } - ); + this.app.get("/login/instance/metadata", (req, res) => { + return res.json({ + title: "YourDash Instance", + message: "Placeholder message. Hey system admin, you should change this!", + loginLayout: LoginLayout.CARDS, + }); + }); // TODO: replace this with tRPC - this.app.get( - "/login/user/:username", - async (req, res) => { - const user = new User( - (req.params as unknown as { username: string }).username - ); - if (await user.doesExist()) { - res.status(200); - return res.json({ - name: { - first: await user.getForename(), - last: await user.getSurname() - } - }); - } else { - res.status(404); - return res.json({ error: "Unknown user" }); - } + this.app.get("/login/user/:username", async (req, res) => { + const user = new User((req.params as unknown as { username: string }).username); + if (await user.doesExist()) { + res.status(200); + return res.json({ + name: { + first: await user.getForename(), + last: await user.getSurname(), + }, + }); + } else { + res.status(404); + return res.json({ error: "Unknown user" }); } - ); + }); - this.app.get( - "/login/user/:username/avatar", - async (req, res) => { - const user = new User( - (req.params as unknown as { username: string }).username - ); - if (await user.doesExist()) { - res.status(200); - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.UserSystemDirectory( - user.username - ), - "avatar128.webp" - ) - ); - } else { - return res.status(404); - } + this.app.get("/login/user/:username/avatar", async (req, res) => { + const user = new User((req.params as unknown as { username: string }).username); + if (await user.doesExist()) { + res.status(200); + return res.sendFile(path.join(this.instance.filesystem.commonPaths.UserSystemDirectory(user.username), "avatar128.webp")); + } else { + return res.status(404); } - ); - - this.app.post( - "/login/user/authenticate", - async (req, res) => { - const body = req.body as { username: string; password: string }; - const user = new User(body.username); - - if (await user.doesExist()) { - res.status(200); + }); - // perform authorization - let sessionToken = await this.instance.authorization.authenticateUser( - body.username, - body.password, - YourDashSessionType.WEB - ); + this.app.post("/login/user/authenticate", async (req, res) => { + const body = req.body as { username: string; password: string }; + const user = new User(body.username); - if (sessionToken === null) { - res.status(401); - return { error: "Password did not match." }; - } + if (await user.doesExist()) { + res.status(200); - res.cookie("authorization", sessionToken, { - httpOnly: true, - maxAge: 7 * 86400, - path: "/", - sameSite: "none", - secure: true - }); + // perform authorization + let sessionToken = await this.instance.authorization.authenticateUser( + body.username, + body.password, + YourDashSessionType.WEB, + ); - return res.json({ success: true }); - } else { - return res.status(404); + if (sessionToken === null) { + res.status(401); + return { error: "Password did not match." }; } - } - ); - - this.app.post( - "/login/user/logout", - async (req, res) => { - res.clearCookie("authorization"); - res.status(200); + res.cookie("authorization", sessionToken, { + httpOnly: true, + maxAge: 7 * 86400, + path: "/", + sameSite: "none", + secure: true, + }); return res.json({ success: true }); + } else { + return res.status(404); } - ); + }); - this.app.get( - "/login/instance/background", - async (req, res) => { - res.status(200); - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.SystemDirectory(), - "loginBackground.avif" - ) - ); - } - ); + this.app.post("/login/user/logout", async (req, res) => { + res.clearCookie("authorization"); - this.app.get( - "/core/login/notice", - async (req, res) => { - return res.json({ - author: "System", - message: "This is a sample notice message", - timestamp: Date.now(), - display: true - }); - } - ); + res.status(200); - this.app.get( - "/login/is-authenticated", - async (req, res) => { - const authorization = req.cookies["authorization"]; + return res.json({ success: true }); + }); - if (!authorization) { - return res.status(401).send(); - } + this.app.get("/login/instance/background", async (req, res) => { + res.status(200); + return res.sendFile(path.join(this.instance.filesystem.commonPaths.SystemDirectory(), "loginBackground.avif")); + }); - const [ username, sessionToken ] = authorization.split(" "); + this.app.get("/core/login/notice", async (req, res) => { + return res.json({ + author: "System", + message: "This is a sample notice message", + timestamp: Date.now(), + display: true, + }); + }); - if ( - !(await this.instance.authorization.authorizeUser( - username, - `${username} ${sessionToken}` - )) - ) { - return res.status(401).send(); - } + this.app.get("/login/is-authenticated", async (req, res) => { + const authorization = req.cookies["authorization"]; - return res.status(200).send(); + if (!authorization) { + return res.status(401).send(); } - ); + + const [username, sessionToken] = authorization.split(" "); + + if (!(await this.instance.authorization.authorizeUser(username, `${username} ${sessionToken}`))) { + return res.status(401).send(); + } + + return res.status(200).send(); + }); this.app.get("/panel/logo/small", async (req, res) => { - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.SystemDirectory(), - "instanceLogo32.webp" - ) - ); + return res.sendFile(path.join(this.instance.filesystem.commonPaths.SystemDirectory(), "instanceLogo32.webp")); }); this.app.get("/panel/logo/medium", async (req, res) => { - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.SystemDirectory(), - "instanceLogo40.webp" - ) - ); + return res.sendFile(path.join(this.instance.filesystem.commonPaths.SystemDirectory(), "instanceLogo40.webp")); }); this.app.get("/panel/logo/large", async (req, res) => { - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.SystemDirectory(), - "instanceLogo128.webp" - ) - ); + return res.sendFile(path.join(this.instance.filesystem.commonPaths.SystemDirectory(), "instanceLogo128.webp")); }); - this.app.get( - "/core/panel/applications", - async (req, res) => { - const applications = this.instance.applications.loadedApplications; + this.app.get("/core/panel/applications", async (req, res) => { + const applications = this.instance.applications.loadedApplications; - return applications.map((app) => { - return res.json({ - id: app._applicationParameters.id, - displayName: app._applicationParameters.displayName, - description: app._applicationParameters.description, - type: "frontend", - endpoint: `/app/a/${app._applicationParameters.id}/` - }); + return applications.map((app) => { + return res.json({ + id: app._applicationParameters.id, + displayName: app._applicationParameters.displayName, + description: app._applicationParameters.description, + type: "frontend", + endpoint: `/app/a/${app._applicationParameters.id}/`, }); - } - ); - - this.app.get( - "/core/panel/applications/app/largeGrid/:applicationId", - async (req, res) => { - const applicationId = (req.params as { applicationId: string }).applicationId; - - const app = this.instance.applications.loadedApplications.find( - (a) => a._applicationParameters.id === applicationId - ); - - if (!app) { - return res.status(404); - } + }); + }); - if ( - await this.instance.filesystem.doesPathExist( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id, - "largeGridIcon.webp" - ) - ) - ) { - // return this.sendFile(res, path.join(app?._applicationSourcePath, "./icon.avif"), "image/avif"); - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id, - "largeGridIcon.webp" - ) - ); - } + this.app.get("/core/panel/applications/app/largeGrid/:applicationId", async (req, res) => { + const applicationId = (req.params as { applicationId: string }).applicationId; - await fs.mkdir( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id - ), - { - recursive: true - } - ); + const app = this.instance.applications.loadedApplications.find((a) => a._applicationParameters.id === applicationId); - if ( - !(await this.instance.filesystem.doesPathExist( - path.join(app?._applicationSourcePath, "./assets/icon.png") - )) - ) { - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel", - "invalidIcon.webp" - ) - ); - } + if (!app) { + return res.status(404); + } - await resizeImage( - path.join(app?._applicationSourcePath, "./assets/icon.png"), - 88, - 88, + if ( + await this.instance.filesystem.doesPathExist( path.join( this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id, - "largeGridIcon.webp" + "largeGridIcon.webp", ), - "webp" - ); - + ) + ) { + // return this.sendFile(res, path.join(app?._applicationSourcePath, "./icon.avif"), "image/avif"); return res.sendFile( path.join( this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id, - "largeGridIcon.webp" - ) + "largeGridIcon.webp", + ), ); } - ); - this.app.get( - "/core/panel/applications/app/smallGrid/:applicationId", - async (req, res) => { - const applicationId = (req.params as { applicationId: string }).applicationId; + await fs.mkdir( + path.join(this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id), + { + recursive: true, + }, + ); - const app = this.instance.applications.loadedApplications.find( - (a) => a._applicationParameters.id === applicationId - ); + if (!(await this.instance.filesystem.doesPathExist(path.join(app?._applicationSourcePath, "./assets/icon.png")))) { + return res.sendFile(path.join(this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel", "invalidIcon.webp")); + } - if (!app) { - return res.status(404); - } + await resizeImage( + path.join(app?._applicationSourcePath, "./assets/icon.png"), + 88, + 88, + path.join( + this.instance.filesystem.commonPaths.GlobalCacheDirectory(), + "panel/applications", + app._applicationParameters.id, + "largeGridIcon.webp", + ), + "webp", + ); - if ( - await this.instance.filesystem.doesPathExist( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id, - "smallGridIcon.webp" - ) - ) - ) { - // return this.sendFile(res, path.join(app?._applicationSourcePath, "./icon.avif"), "image/avif"); - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id, - "smallGridIcon.webp" - ) - ); - } + return res.sendFile( + path.join( + this.instance.filesystem.commonPaths.GlobalCacheDirectory(), + "panel/applications", + app._applicationParameters.id, + "largeGridIcon.webp", + ), + ); + }); - await fs.mkdir( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id - ), - { - recursive: true - } - ); + this.app.get("/core/panel/applications/app/smallGrid/:applicationId", async (req, res) => { + const applicationId = (req.params as { applicationId: string }).applicationId; - if ( - !(await this.instance.filesystem.doesPathExist( - path.join(app?._applicationSourcePath, "./assets/icon.png") - )) - ) { - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel", - "invalidIcon.webp" - ) - ); - } + const app = this.instance.applications.loadedApplications.find((a) => a._applicationParameters.id === applicationId); + + if (!app) { + return res.status(404); + } - await resizeImage( - path.join(app?._applicationSourcePath, "./assets/icon.png"), - 88, - 88, + if ( + await this.instance.filesystem.doesPathExist( path.join( this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id, - "smallGridIcon.webp" + "smallGridIcon.webp", ), - "webp" - ); - + ) + ) { + // return this.sendFile(res, path.join(app?._applicationSourcePath, "./icon.avif"), "image/avif"); return res.sendFile( path.join( this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id, - "smallGridIcon.webp" - ) + "smallGridIcon.webp", + ), ); } - ); - this.app.get( - "/core/panel/applications/app/list/:applicationId", - async (req, res) => { - const applicationId = (req.params as { applicationId: string }).applicationId; + await fs.mkdir( + path.join(this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id), + { + recursive: true, + }, + ); - const app = this.instance.applications.loadedApplications.find( - (a) => a._applicationParameters.id === applicationId - ); + if (!(await this.instance.filesystem.doesPathExist(path.join(app?._applicationSourcePath, "./assets/icon.png")))) { + return res.sendFile(path.join(this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel", "invalidIcon.webp")); + } - if (!app) { - return res.status(404); - } + await resizeImage( + path.join(app?._applicationSourcePath, "./assets/icon.png"), + 88, + 88, + path.join( + this.instance.filesystem.commonPaths.GlobalCacheDirectory(), + "panel/applications", + app._applicationParameters.id, + "smallGridIcon.webp", + ), + "webp", + ); - if ( - await this.instance.filesystem.doesPathExist( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id, - "listIcon.webp" - ) - ) - ) { - // return this.sendFile(res, path.join(app?._applicationSourcePath, "./icon.avif"), "image/avif"); - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id, - "listIcon.webp" - ) - ); - } + return res.sendFile( + path.join( + this.instance.filesystem.commonPaths.GlobalCacheDirectory(), + "panel/applications", + app._applicationParameters.id, + "smallGridIcon.webp", + ), + ); + }); - await fs.mkdir( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id - ), - { - recursive: true - } - ); + this.app.get("/core/panel/applications/app/list/:applicationId", async (req, res) => { + const applicationId = (req.params as { applicationId: string }).applicationId; - if ( - !(await this.instance.filesystem.doesPathExist( - path.join(app?._applicationSourcePath, "./assets/icon.png") - )) - ) { - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel", - "invalidIcon.webp" - ) - ); - } + const app = this.instance.applications.loadedApplications.find((a) => a._applicationParameters.id === applicationId); + + if (!app) { + return res.status(404); + } - await resizeImage( - path.join(app?._applicationSourcePath, "./assets/icon.png"), - 88, - 88, + if ( + await this.instance.filesystem.doesPathExist( path.join( this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id, - "listIcon.webp" + "listIcon.webp", ), - "webp" - ); - + ) + ) { + // return this.sendFile(res, path.join(app?._applicationSourcePath, "./icon.avif"), "image/avif"); return res.sendFile( path.join( this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id, - "listIcon.webp" - ) + "listIcon.webp", + ), ); } - ); + + await fs.mkdir( + path.join(this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id), + { + recursive: true, + }, + ); + + if (!(await this.instance.filesystem.doesPathExist(path.join(app?._applicationSourcePath, "./assets/icon.png")))) { + return res.sendFile(path.join(this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel", "invalidIcon.webp")); + } + + await resizeImage( + path.join(app?._applicationSourcePath, "./assets/icon.png"), + 88, + 88, + path.join( + this.instance.filesystem.commonPaths.GlobalCacheDirectory(), + "panel/applications", + app._applicationParameters.id, + "listIcon.webp", + ), + "webp", + ); + + return res.sendFile( + path.join( + this.instance.filesystem.commonPaths.GlobalCacheDirectory(), + "panel/applications", + app._applicationParameters.id, + "listIcon.webp", + ), + ); + }); /* async (req, res) => { @@ -875,163 +787,132 @@ class RequestManager { ); */ - this.app.get( - "/core/panel/quick-shortcuts", - async (req, res) => { - const username = this.getRequestUsername(); - const query = await this.instance.database`SELECT pinned_applications + this.app.get("/core/panel/quick-shortcuts", async (req, res) => { + const username = this.getRequestUsername(); + const query = await this.instance.database`SELECT pinned_applications FROM public.panel_configuration WHERE username = ${username}`; - const pinnedApplications: YourDashApplication[] = - query[0].pinned_applications.map((a: string) => - this.instance.applications.loadedApplications.find( - (i) => i._applicationParameters.id === a - ) - ).filter((a: YourDashApplication | undefined) => a !== undefined); - - return pinnedApplications.map((app) => { - if (app._applicationParameters.frontend) { - return res.json({ - displayName: app._applicationParameters.displayName, - id: app._applicationParameters.id, - endpoint: `/app/a/${app._applicationParameters.id}` - }); - } + const pinnedApplications: YourDashApplication[] = query[0].pinned_applications + .map((a: string) => this.instance.applications.loadedApplications.find((i) => i._applicationParameters.id === a)) + .filter((a: YourDashApplication | undefined) => a !== undefined); + return pinnedApplications.map((app) => { + if (app._applicationParameters.frontend) { return res.json({ - displayName: app._applicationParameters.displayName, - id: app._applicationParameters.id, - url: - app._applicationParameters.externalFrontend?.url || "example.com" - }); - }); - } - ); - - this.app.get( - "/core/panel/quick-shortcut/icon/:applicationId", - async (req, res) => { - const applicationId = (req.params as { applicationId: string }).applicationId; + displayName: app._applicationParameters.displayName, + id: app._applicationParameters.id, + endpoint: `/app/a/${app._applicationParameters.id}`, + }); + } - const app = this.instance.applications.loadedApplications.find( - (a) => a._applicationParameters.id === applicationId - ); + return res.json({ + displayName: app._applicationParameters.displayName, + id: app._applicationParameters.id, + url: app._applicationParameters.externalFrontend?.url || "example.com", + }); + }); + }); - if (!app) { - return res.status(404); - } + this.app.get("/core/panel/quick-shortcut/icon/:applicationId", async (req, res) => { + const applicationId = (req.params as { applicationId: string }).applicationId; - if ( - await this.instance.filesystem.doesPathExist( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id, - "quickShortcut.webp" - ) - ) - ) { - // return this.sendFile(res, path.join(app?._applicationSourcePath, "./icon.avif"), "image/avif"); - return res.sendFile( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id, - "quickShortcut.webp" - ) - ); - } + const app = this.instance.applications.loadedApplications.find((a) => a._applicationParameters.id === applicationId); - await fs.mkdir( - path.join( - this.instance.filesystem.commonPaths.GlobalCacheDirectory(), - "panel/applications", - app._applicationParameters.id - ), - { - recursive: true - } - ); + if (!app) { + return res.status(404); + } - await resizeImage( - path.join(app?._applicationSourcePath, "./assets/icon.png"), - 88, - 88, + if ( + await this.instance.filesystem.doesPathExist( path.join( this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id, - "quickShortcut.webp" + "quickShortcut.webp", ), - "webp" - ); - + ) + ) { + // return this.sendFile(res, path.join(app?._applicationSourcePath, "./icon.avif"), "image/avif"); return res.sendFile( path.join( this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id, - "quickShortcut.webp" - ) + "quickShortcut.webp", + ), ); } - ); - this.app.post( - "/core/panel/quick-shortcuts/create", - async (req, res) => { - const applicationId = (req.body as { id: string }).id; - const username = this.getRequestUsername(); - - if ( - this.instance.applications.loadedApplications.find( - (i) => i._applicationParameters.id === applicationId - ) - ) { - try { - const previousPins = await this.instance.database`SELECT pinned_applications + await fs.mkdir( + path.join(this.instance.filesystem.commonPaths.GlobalCacheDirectory(), "panel/applications", app._applicationParameters.id), + { + recursive: true, + }, + ); + + await resizeImage( + path.join(app?._applicationSourcePath, "./assets/icon.png"), + 88, + 88, + path.join( + this.instance.filesystem.commonPaths.GlobalCacheDirectory(), + "panel/applications", + app._applicationParameters.id, + "quickShortcut.webp", + ), + "webp", + ); + + return res.sendFile( + path.join( + this.instance.filesystem.commonPaths.GlobalCacheDirectory(), + "panel/applications", + app._applicationParameters.id, + "quickShortcut.webp", + ), + ); + }); + + this.app.post("/core/panel/quick-shortcuts/create", async (req, res) => { + const applicationId = (req.body as { id: string }).id; + const username = this.getRequestUsername(); + + if (this.instance.applications.loadedApplications.find((i) => i._applicationParameters.id === applicationId)) { + try { + const previousPins = await this.instance.database`SELECT pinned_applications FROM public.panel_configuration WHERE username = ${username};`; - if ( - previousPins[0].pinned_applications.includes(applicationId) - ) { - return { success: false }; - } + if (previousPins[0].pinned_applications.includes(applicationId)) { + return { success: false }; + } - const newPins = [ - ...previousPins[0].pinned_applications, - applicationId - ]; + const newPins = [...previousPins[0].pinned_applications, applicationId]; - await this.instance.database`UPDATE public.panel_configuration + await this.instance.database`UPDATE public.panel_configuration SET pinned_applications = ${newPins} WHERE username = ${username};`; - return { success: true }; - } catch (error) { - return { success: false }; - } + return { success: true }; + } catch (error) { + return { success: false }; } } - ); + }); - this.app.get( - "/core/panel", - async (req, res) => { - const username = this.getRequestUsername(); + this.app.get("/core/panel", async (req, res) => { + const username = this.getRequestUsername(); - const dbquery = await this.instance.database`SELECT widgets, size + const dbquery = await this.instance.database`SELECT widgets, size FROM public.panel_configuration WHERE username = ${username};`; - return { - widgets: dbquery[0].widgets, - size: dbquery[0].size - }; - } - ); + return { + widgets: dbquery[0].widgets, + size: dbquery[0].size, + }; + }); this.app.use( // @ts-ignore @@ -1039,10 +920,9 @@ class RequestManager { if (!error.cause) { console.error(error); return res.json({ - status: 500, - message: - "Internal server error, if you are a developer or administrator, please check the console." - }); + status: 500, + message: "Internal server error, if you are a developer or administrator, please check the console.", + }); } const cause: { @@ -1059,64 +939,55 @@ class RequestManager { // noinspection SuspiciousTypeOfGuard if (cause.name === "ZodError") { res.status(400).json({ - statusCode: 400, - error: "Bad Request", // @ts-ignore - httpPart: error.httpPart!, - issues: cause.issues - }); + statusCode: 400, + error: "Bad Request", // @ts-ignore + httpPart: error.httpPart!, + issues: cause.issues, + }); } else { // Log error this.instance.log.error("request_manager", error); // Send error response res.status(500).json({ ok: false }); } - } + }, ); } // start listening for requests // once called, no more routes may be defined async __internal_beginListening() { - this.instance.log.info( - "request_manager", - "Starting RequestManager Listening..." - ); + this.instance.log.info("request_manager", "Starting RequestManager Listening..."); try { this.app.listen({ - port: this.instance.configurationManager.config.port, - host: "0.0.0.0" - }); + port: this.instance.configurationManager.config.port, + host: "0.0.0.0", + }); this.instance.log.info( "request_manager", `YourDash Instance Backend Online & listening at ${this.instance.log.addEmphasisToString( - `port ${this.instance.configurationManager.config.port}`)}` + `port ${this.instance.configurationManager.config.port}`, + )}`, ); this.instance.log.info("request_manager", `Attempting to ping self`); fetch( - `http://localhost:${this.instance.configurationManager.config.port}/core/test/self-ping` + `http://localhost:${this.instance.configurationManager.config.port}/core/test/self-ping`, // @ts-ignore - ).then((resp) => resp.json()) + ) + .then((resp) => resp.json()) // @ts-ignore - .then((data: { success?: boolean }) => { - if (data?.success) { - this.instance.setStatus(InstanceStatus.OK); - return this.instance.log.success( - "self_ping_test", - "self ping successful - The server is receiving requests" - ); - } - - this.instance.log.error( - "request_manager", - "CRITICAL ERROR!, unable to ping self" - ); - }).catch(() => { - this.instance.log.error( - "request_manager", - "CRITICAL ERROR!, unable to ping self" - ); - }); + .then((data: { success?: boolean }) => { + if (data?.success) { + this.instance.setStatus(InstanceStatus.OK); + return this.instance.log.success("self_ping_test", "self ping successful - The server is receiving requests"); + } + + this.instance.log.error("request_manager", "CRITICAL ERROR!, unable to ping self"); + }) + .catch(() => { + this.instance.log.error("request_manager", "CRITICAL ERROR!, unable to ping self"); + }); } catch (err) { this.instance.log.error("request_manager", err); console.error(err); @@ -1137,4 +1008,4 @@ class RequestManager { export default RequestManager; -export type AppRouter = RequestManager["tRPCRouter"]; \ No newline at end of file +export type AppRouter = RequestManager["tRPCRouter"]; diff --git a/packages/instance/src/index.ts b/packages/instance/src/index.ts index 6b15d48..baba707 100644 --- a/packages/instance/src/index.ts +++ b/packages/instance/src/index.ts @@ -167,9 +167,32 @@ class Instance { process.stdout.cursorTo(0, 0); process.stdout.clearScreenDown(); - process.exit(0); + process.stdout.cursorTo(0, 0); - return this; + const goodbye = [ + "Goodbye!", + "Chao", + "Salut", + "Ciao", + "Tschüss", + "じゃあね", + "拜拜", + "Tchau", + "Пока", + "잘 가", + "Hej då", + "Doei", + "Γεια", + "Na razie", + "Güle güle", + "Adeus", + "Tot ziens", + "Hẹn gặp lại", + ]; + + process.stdout.write("Shutdown completed! -> "); + process.stdout.write(goodbye[Math.floor(Math.random() * goodbye.length)]); + process.exit(0); } } diff --git a/packages/instance/src/log.ts b/packages/instance/src/log.ts index ebdafed..6ad8c3c 100644 --- a/packages/instance/src/log.ts +++ b/packages/instance/src/log.ts @@ -28,18 +28,31 @@ class Logger { global.console.log = (...data: any[]): void => { process.stdout.cursorTo(0, this._internal_getWindowSize()[1], () => { - process.stdout.clearLine(1); - for (const d of data) { - if (d.toString !== undefined) { - process.stdout.write(util.format(d)); + process.stdout.clearLine(1, () => { + let writtenData = ""; + for (const d of data) { + if (d.toString !== undefined) { + writtenData += util.format(d); + } else { + writtenData += util.inspect(d, { + compact: false, + colors: true, + depth: 3, + breakLength: this._internal_getWindowSize()[0] - this.log.META_LENGTH + 6, + }); + } + writtenData += " "; + } + if (writtenData.endsWith("\n")) { + process.stdout.write(writtenData, () => { + this._internal_writePrompt(); + }); } else { - process.stdout.write(util.inspect(d)); + process.stdout.write(writtenData + "\n", () => { + this._internal_writePrompt(); + }); } - process.stdout.write(" "); - } - process.stdout.write("\n"); - - this._internal_writePrompt(); + }); }); }; @@ -62,9 +75,10 @@ class Logger { return this.logMessage(LogType.PROMPT, ...message); } - removeCommandPrompt() { + removeCommandPrompt(cb: () => void) { process.stdout.cursorTo(0, process.stdout.getWindowSize()[1] - 1, () => { process.stdout.clearScreenDown(); + cb(); }); return this; @@ -185,7 +199,7 @@ class Logger { process.stdout.cursorTo(this.log.META_LENGTH + 6, this._internal_getWindowSize()[1], () => { if (this.log.instance.subSystems.configuration?.hasFeature(WorkspacesFeatureFlags.SlashCommands)) { // write the prompt indicator to the stdout - process.stdout.write(`> ${this.log.instance.subSystems.consoleCommands?.rlInterface?.line}`); + process.stdout.write(`> ${this.log.instance.subSystems.consoleCommands?.rlInterface?.line || ""}`); } else { process.stdout.write(" "); } diff --git a/packages/instance/src/subSystems.ts b/packages/instance/src/subSystems.ts index e9b3324..daf2c9e 100644 --- a/packages/instance/src/subSystems.ts +++ b/packages/instance/src/subSystems.ts @@ -34,13 +34,12 @@ export default abstract class SubSystem { } async startup(): Promise { - this.instance.log.system.info("Starting up..."); + this.log.info("Starting up..."); return true; } stop() { - this.instance.log.system.info(`Stopping SubSystem ${this.id}`); - + this.log.info(`Stopping SubSystem ${this.id}`); return this; } } diff --git a/packages/instance/src/subsystems/authorization.ts b/packages/instance/src/subsystems/authorization.ts index a9697f2..1effd97 100644 --- a/packages/instance/src/subsystems/authorization.ts +++ b/packages/instance/src/subsystems/authorization.ts @@ -1,8 +1,5 @@ import type { Instance } from "../index.js"; import SubSystem from "../subSystems.js"; -import { USERS_DATABASE_CONNECTION_ID } from "./users.js"; - -export const AUTHORIZATION_SESSIONS_DATABASE_CONNECTION_ID = "databases/authorization_sessions"; export enum AuthorizedDeviceType { Desktop, @@ -21,7 +18,7 @@ export default class AuthorizationSubsystem extends SubSystem { // @returns {string} the new session's sessionToken async createSession(userId: number, password: string, deviceId: AuthorizedDeviceType): Promise { try { - const usersDb = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const usersDb = this.instance.subSystems.database.db(); if ( !(await Bun.password.verify( @@ -31,7 +28,7 @@ export default class AuthorizationSubsystem extends SubSystem { ) return undefined; - const sessionsDb = this.instance.subSystems.database.getConnection(AUTHORIZATION_SESSIONS_DATABASE_CONNECTION_ID); + const sessionsDb = this.instance.subSystems.database.db(); this.log.info("Password entered matched the hashed password."); @@ -54,7 +51,7 @@ export default class AuthorizationSubsystem extends SubSystem { async verifySession(sessionToken: string): Promise { const [_, userId, token] = sessionToken.split(":"); - const sessionsDb = this.instance.subSystems.database.getConnection(AUTHORIZATION_SESSIONS_DATABASE_CONNECTION_ID); + const sessionsDb = this.instance.subSystems.database.db(); const session = ( await sessionsDb`SELECT session_id, valid_until FROM Sessions WHERE user_id = ${userId} AND session_token = ${token}` @@ -74,7 +71,7 @@ export default class AuthorizationSubsystem extends SubSystem { // @returns {true} successful // @returns {false} failed async setPassword(userId: number, password: string): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); if (!(await this.instance.subSystems.users.doesUserExist(userId))) { return false; @@ -90,7 +87,7 @@ export default class AuthorizationSubsystem extends SubSystem { async startup() { // loop through all users, check for any session tokens which are expired and remove them from the user's valud sessions pool - const db = this.instance.subSystems.database.getConnection(AUTHORIZATION_SESSIONS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); // init the sessions database // @@ -99,7 +96,13 @@ export default class AuthorizationSubsystem extends SubSystem { // session_token - the session's access token in the format 'workspaces_session:[user_id]:[token]' (string) // device_id - the session's device type (AuthorizedDeviceType) // valid_until - the epoch time which when reached, the session will be invalid (number) - await db`CREATE TABLE IF NOT EXISTS Sessions (session_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, session_token TEXT, device_id INTEGER, valid_until BIGINT)`; + await db`CREATE TABLE IF NOT EXISTS Sessions ( + session_id SERIAL PRIMARY KEY, + user_id INTEGER, + session_token TEXT, + device_id INTEGER, + valid_until BIGINT + )`; return true; } diff --git a/packages/instance/src/subsystems/configuration.ts b/packages/instance/src/subsystems/configuration.ts index d3fbcaa..25da7d3 100644 --- a/packages/instance/src/subsystems/configuration.ts +++ b/packages/instance/src/subsystems/configuration.ts @@ -26,7 +26,7 @@ export default class ConfigurationSubsystem extends SubSystem { postgres: { // TODO: actually set these values user: "postgres", - password: "password", + password: "postgres", host: "localhost", port: 5432, database: "tricolor_workspaces", diff --git a/packages/instance/src/subsystems/consoleCommands.ts b/packages/instance/src/subsystems/consoleCommands.ts index 881c6d2..eac5db7 100644 --- a/packages/instance/src/subsystems/consoleCommands.ts +++ b/packages/instance/src/subsystems/consoleCommands.ts @@ -68,7 +68,7 @@ export default class ConsoleCommandsSubsytem extends SubSystem { process.stdin.on("keypress", async (str, key) => { if (key.name === "up") { - if (historyIndex < 1) { + if (historyIndex === 0) { return; } @@ -85,6 +85,14 @@ export default class ConsoleCommandsSubsytem extends SubSystem { return; } else if (key.name === "down") { if (historyIndex + 1 > self.commandHistory.length - 1) { + line = ""; + cursorPos = CURSOR_MIN_POS; + + process.stdout.cursorTo(CURSOR_MIN_POS); + process.stdout.clearLine(1); + process.stdout.write(line); + process.stdout.cursorTo(cursorPos); + return; } historyIndex++; diff --git a/packages/instance/src/subsystems/consoleCommands/commands/password.ts b/packages/instance/src/subsystems/consoleCommands/commands/password.ts index d7cbba8..05f6ec7 100644 --- a/packages/instance/src/subsystems/consoleCommands/commands/password.ts +++ b/packages/instance/src/subsystems/consoleCommands/commands/password.ts @@ -1,4 +1,3 @@ -import { USERS_DATABASE_CONNECTION_ID } from "../../users.js"; import Command, { type ICommandRuntimeParameters } from "../command.js"; export default class ExitCommand extends Command { @@ -22,7 +21,7 @@ export default class ExitCommand extends Command { self.instance.subSystems.consoleCommands.currentCommandInterface.cb = async (data) => { password = data.trim(); if (password !== "") { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); const { id: userId } = (await db`SELECT id FROM Users WHERE username = ${username}`)?.[0] || { id: undefined }; diff --git a/packages/instance/src/subsystems/consoleCommands/commands/restart.ts b/packages/instance/src/subsystems/consoleCommands/commands/restart.ts index 5bcb61a..a0be35b 100644 --- a/packages/instance/src/subsystems/consoleCommands/commands/restart.ts +++ b/packages/instance/src/subsystems/consoleCommands/commands/restart.ts @@ -1,27 +1,26 @@ -import Command, {type ICommandRuntimeParameters} from "../command.js"; +import Command, { type ICommandRuntimeParameters } from "../command.js"; export default class RestartCommand extends Command { - commandId = "restart"; - flags = {}; - aliases = [ "rs" ]; - shortDescription = "Restart the Workspaces instance"; + commandId = "restart"; + flags = {}; + aliases = ["rs"]; + shortDescription = "Restart the Workspaces instance"; - async run(parameters: ICommandRuntimeParameters) { - // this.instance.log.info( - // "command_restart", - // "Restarting Workspaces instance...", - // ); + async run(parameters: ICommandRuntimeParameters) { + // this.instance.log.info( + // "command_restart", + // "Restarting Workspaces instance...", + // ); - this.instance.log.system.error( - "command_restart", - "This command does not work as instance shutdown is unimplemented...", - ); + this.instance.log.system.error("command_restart", "This command does not work as instance shutdown is unimplemented..."); - // TODO: implement a shutdown sequence - // this.instance.shutdown() - // this.instance.requestManager.app.server.close(); - // this.instance.commandManager.close(); + // TODO: implement a shutdown sequence + // this.instance.shutdown() + // this.instance.requestManager.app.server.close(); + // this.instance.commandManager.close(); - return this.finishRun(); - } + this.instance.shutdown(); + + return this.finishRun(); + } } diff --git a/packages/instance/src/subsystems/database.ts b/packages/instance/src/subsystems/database.ts index 57e4d7f..b680c17 100644 --- a/packages/instance/src/subsystems/database.ts +++ b/packages/instance/src/subsystems/database.ts @@ -19,6 +19,8 @@ export default class DatabaseSubsystem extends SubSystem { path.join(this.instance.subSystems.filesystem.FS_ROOT, "databases"), ); + this.createPostgresConnection(); + return true; } @@ -28,6 +30,10 @@ export default class DatabaseSubsystem extends SubSystem { return this.createSQLiteConnection(connectionId); } + db() { + return this.getConnection("postgres"); + } + private createSQLiteConnection(connectionId: string) { let conPath = "sqlite://" + path.join(this.instance.subSystems.filesystem.FS_ROOT, connectionId) + ".sqlite"; diff --git a/packages/instance/src/subsystems/notifications.ts b/packages/instance/src/subsystems/notifications.ts index f2cdaca..ea51859 100644 --- a/packages/instance/src/subsystems/notifications.ts +++ b/packages/instance/src/subsystems/notifications.ts @@ -1,10 +1,21 @@ -import type {Instance} from "../index.js"; +import type { Instance } from "../index.js"; import SubSystem from "../subSystems.js"; +export enum WorkspacesNoticeType { + Login, + Signup, +} + export default class NotificationsSubsystem extends SubSystem { constructor(instance: Instance) { - super("filesystem", instance) + super("notifications", instance); + + return this; + } + // TODO: implement this + applyNotice(targetUserId: number, noticeType: WorkspacesNoticeType[], noticeTitle: string, noticeBody: string) { + this.log.warning("Notices are Unimplemented"); return this; } -} \ No newline at end of file +} diff --git a/packages/instance/src/subsystems/trpc.ts b/packages/instance/src/subsystems/trpc.ts index 656bda4..7eef2fa 100644 --- a/packages/instance/src/subsystems/trpc.ts +++ b/packages/instance/src/subsystems/trpc.ts @@ -3,7 +3,6 @@ import z from "zod"; import { Instance } from "../index.js"; import { AuthorizedDeviceType } from "./authorization.js"; import { BunRequest } from "bun"; -import { USERS_DATABASE_CONNECTION_ID } from "./users.js"; export const createTRPCContext = (opt: { rawRequest: { req: BunRequest; resHeaders: Headers }; instance: Instance }) => { return { @@ -71,7 +70,7 @@ export const workspacesRouter = t.router({ .output( z.union([ z.object({ type: z.literal("error"), message: z.string() }), - z.object({ type: z.literal("success"), sessionToken: z.string() }), + z.object({ type: z.literal("success"), sessionToken: z.string(), notice: z.boolean().optional() }), ]), ) .mutation(async (opt) => { @@ -173,7 +172,7 @@ export const workspacesRouter = t.router({ navigation: { user: { name: procedure.output(z.object({ username: z.string(), forename: z.string(), surname: z.string() })).query(async (opt) => { - const db = opt.ctx.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = opt.ctx.instance.subSystems.database.db(); const user = (await db`SELECT username, forename, surname FROM Users WHERE id = ${opt.ctx.userId};`)?.[0]; diff --git a/packages/instance/src/subsystems/users.ts b/packages/instance/src/subsystems/users.ts index f93ad41..a3f4f01 100644 --- a/packages/instance/src/subsystems/users.ts +++ b/packages/instance/src/subsystems/users.ts @@ -5,8 +5,6 @@ import path from "path"; import { promises as fs } from "fs"; import sharp from "sharp"; -export const USERS_DATABASE_CONNECTION_ID = "databases/users"; - export interface IUserDatabaseUser { id: number; username: string; @@ -34,20 +32,24 @@ export class WorkspacesUser { // @returns {false} failed to change the username // @returns {true} successfully changed the username async setUsername(username: string): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); - - if ((await db`SELECT username FROM Users WHERE username = ${username}`).count !== 0) return false; + const db = this.instance.subSystems.database.db(); - await db`UPDATE Users SET username = ${username} WHERE id = ${this.userId}`; + try { + if ((await db`SELECT username FROM Users WHERE username = ${username}`).count !== 0) return false; - return true; + await db`UPDATE Users SET username = ${username} WHERE id = ${this.userId}`; + return true; + } catch (err) { + this.instance.subSystems.users.log.error(`Failed to set username for ${this.userId}`); + return false; + } } // Get the user's username // @returns {string} the users username // @returns {undefined} could not get the user's username async getUsername(): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); return (await db`SELECT username FROM Users WHERE id = ${this.userId}`)?.[0]?.username || undefined; } @@ -56,18 +58,23 @@ export class WorkspacesUser { // @returns {false} failed to change the forename // @returns {true} successfully changed the forename async setForename(forename: string): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); - await db`UPDATE Users SET forename = ${forename} WHERE id = ${this.userId}`; + try { + await db`UPDATE Users SET forename = ${forename} WHERE id = ${this.userId}`; - return true; + return true; + } catch (err) { + this.instance.subSystems.users.log.error(`Failed to set forename for ${this.userId}`); + return false; + } } // Get the user's forename // @returns {string} the users forename // @returns {undefined} could not get the user's forename async getForename(): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); return (await db`SELECT forename FROM Users WHERE id = ${this.userId}`)?.[0]?.forename || undefined; } @@ -76,18 +83,23 @@ export class WorkspacesUser { // @returns {false} failed to change the surname // @returns {true} successfully changed the surname async setSurname(surname: string): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); - await db`UPDATE Users SET surname = ${surname} WHERE id = ${this.userId}`; + try { + await db`UPDATE Users SET surname = ${surname} WHERE id = ${this.userId}`; - return true; + return true; + } catch (err) { + this.instance.subSystems.users.log.error(`Failed to set surname for ${this.userId}`); + return false; + } } // Get the user's surname // @returns {string} the users surname // @returns {undefined} could not get the user's surname async getSurname(): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); return (await db`SELECT surname FROM Users WHERE id = ${this.userId}`)?.[0]?.surname || undefined; } @@ -115,18 +127,22 @@ export class WorkspacesUser { // @returns {false} failed to set the quota // @returns {true} successfully set the quota async setQuota(quota: number): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); - - await db`UPDATE Users SET storage_quota = ${quota} WHERE id = ${this.userId}`; + const db = this.instance.subSystems.database.db(); - return true; + try { + await db`UPDATE Users SET storage_quota = ${quota} WHERE id = ${this.userId}`; + return true; + } catch (err) { + this.instance.subSystems.users.log.error(`Failed to set storage quota for ${this.userId}`); + return false; + } } // Get the user's quota // @returns {number} the users quota // @returns {undefined} could not get the user's quota async getQuota(): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); return (await db`SELECT storage_quota FROM Users WHERE id = ${this.userId}`)?.[0]?.storage_quota || undefined; } @@ -135,30 +151,89 @@ export class WorkspacesUser { // @returns {false} failed to change the bio // @returns {true} successfully changed the bio async setBio(bio: string): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); - - await db`UPDATE Users SET bio = ${bio} WHERE id = ${this.userId}`; + const db = this.instance.subSystems.database.db(); - return true; + try { + await db`UPDATE Users SET bio = ${bio} WHERE id = ${this.userId}`; + return true; + } catch (err) { + this.instance.subSystems.users.log.error(`Failed to set bio for ${this.userId}`); + return false; + } } // Get the user's bio // @returns {string} the users bio // @returns {undefined} could not get the user's bio async getBio(): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); - return (await db`SELECT bio FROM Users WHERE id = ${this.userId}`)?.[0]?.surname || undefined; + return (await db`SELECT bio FROM Users WHERE id = ${this.userId}`)?.[0]?.bio || undefined; } - // TODO: set email & gender + // Sets the user's email to email + // @returns {false} failed to change the email + // @returns {true} successfully changed the email + async setEmail(email: string): Promise { + const db = this.instance.subSystems.database.db(); + try { + await db`UPDATE Users SET email = ${email} WHERE id = ${this.userId}`; + return true; + } catch (err) { + this.instance.subSystems.users.log.error(`Failed to set email for ${this.userId}`); + return false; + } + } - async setAvatar(avatarFile: string): Promise { - await fs.cp(avatarFile, path.join(this.getPath(), "assets/avatar/avatar.png")); + // Get the user's email + // @returns {string} the users email + // @returns {undefined} could not get the user's email + async getEmail(): Promise { + const db = this.instance.subSystems.database.db(); - return true; + return (await db`SELECT email FROM Users WHERE id = ${this.userId}`)?.[0]?.email || undefined; + } + + // Sets the user's gender to gender + // @returns {false} failed to change the gender + // @returns {true} successfully changed the gender + async setGender(gender: "female" | "male" | "other"): Promise { + const db = this.instance.subSystems.database.db(); + + try { + return await db`UPDATE Users SET gender = ${gender} WHERE id = ${this.userId}`; + return true; + } catch (err) { + this.instance.subSystems.users.log.error(`Failed to set gender for ${this.userId}`); + return false; + } + } + + // Get the user's gender + // @returns {"female" | "male" | "other"} the users gender + // @returns {undefined} could not get the user's gender + async getGender(): Promise { + const db = this.instance.subSystems.database.db(); + + return (await db`SELECT gender FROM Users WHERE id = ${this.userId}`)?.[0]?.gender || undefined; + } + + // sets the user's original quality avatar to avatarFile + // @returns {true} - copied the avatarFile successfully + // @returns {false} - failed to copy the avatarFile + async setAvatar(avatarFile: string): Promise { + try { + await fs.cp(avatarFile, path.join(this.getPath(), "assets/avatar/avatar.png")); + return true; + } catch (err) { + this.instance.subSystems.users.log.error(`Failed to set avatar for ${this.userId} to ${avatarFile}`); + return false; + } } + // generates the required avatar sizes from the user's avatarFile, if override is not set to true, only missing avatar sizes will be generated + // @returns {true} - generated required avatar files successfully + // @returns {false} - failed to generate all required avatar files async generateAvatars(override?: boolean): Promise { const AVATAR_SIZES: { width: number; height: number; name: string }[] = [ { width: 16, height: 16, name: "xs" }, @@ -185,6 +260,8 @@ export class WorkspacesUser { return true; } + // Check that all required directories and assets are valid for this user. + // If they are missing or invalid, directories will be created and assets will be generated / copied async verify() { const USER_DIRECTORIES = [ "/", @@ -229,7 +306,7 @@ export default class UsersSubsystem extends SubSystem { async startup(): Promise { await super.startup(); - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); // init the users databasecurrentCommandInterface // @@ -244,7 +321,19 @@ export default class UsersSubsystem extends SubSystem { // is_email_verified - is the user's chosen contact email verified to be theirs (boolean) // socials - the user's chosen social media links in the format '[name]:-:[url]' as such, the string ':-:' must not be in either [name] or [url] (string[]) // hashed_password - the user's password after it has been hashed by bun (string) - await db`CREATE TABLE IF NOT EXISTS Users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, forename TEXT, surname TEXT, gender TEXT, bio TEXT, storage_quota BIGINT, email TEXT, is_email_verified BOOL, socials TEXT[], hashed_password TEXT)`; + await db`CREATE TABLE IF NOT EXISTS Users ( + id SERIAL PRIMARY KEY, + username TEXT, + forename TEXT, + surname TEXT, + gender TEXT DEFAULT 'other', + bio TEXT, + storage_quota BIGINT, + email TEXT, + is_email_verified BOOL DEFAULT FALSE, + socials TEXT[] DEFAULT '{}', + hashed_password TEXT + )`; let administratorUserId = await this.createUser("admin"); @@ -268,7 +357,7 @@ export default class UsersSubsystem extends SubSystem { // @returns {number} the userId of the created user // @returns {undefined} the user already exists async createUser(username: string): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); if ((await db`SELECT username FROM Users WHERE username = ${username}`).count !== 0) return undefined; @@ -298,27 +387,35 @@ export default class UsersSubsystem extends SubSystem { // @returns {true} they exist // @returns {false} they do not exist async doesUserExist(userId: number): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); if ((await db`SELECT username FROM Users WHERE id = ${userId}`).count === 1) return true; return false; } + // Gets an array of all users registered on this instance + // @returns {WorkspacesUser[]} an array of all users async getAllUsers(): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); - return (await db`SELECT id FROM Users`).map((u: number) => new WorkspacesUser(this.instance, u.id)); + return (await db`SELECT id FROM Users`).map((u: { id: number }) => new WorkspacesUser(this.instance, u.id)); } + // Gets a user by their userId + // @returns {WorkspacesUser} the user + // @returns {undefined} no such user exists async getUserById(userId: number): Promise { if (this.doesUserExist(userId) === undefined) return undefined; return new WorkspacesUser(this.instance, userId); } + // Gets a user by their username + // @returns {WorkspacesUser} the user + // @returns {undefined} no such user exists async getUserByUsername(username: string): Promise { - const db = this.instance.subSystems.database.getConnection(USERS_DATABASE_CONNECTION_ID); + const db = this.instance.subSystems.database.db(); const userId = (await db`SELECT id FROM Users WHERE username = ${username}`)?.[0]?.id; diff --git a/packages/uikit-solid/package.json b/packages/uikit-solid/package.json index f4dc45d..8ff96a7 100644 --- a/packages/uikit-solid/package.json +++ b/packages/uikit-solid/package.json @@ -4,7 +4,7 @@ "description": "The Tricolor Workspaces UI framework for solid.js", "repository": { "type": "git", - "url": "https://git.tcsw.uk/tricolor/workspaces" + "url": "https://github.com/tricolorsoftware/workspaces" }, "dependencies": { "@solid-primitives/media": "^2.3.3", -- 2.51.2 From e4d29da82f6de32346850ae08067c070e6ac4164 Mon Sep 17 00:00:00 2001 From: ewsgit Date: Mon, 3 Nov 2025 00:50:38 +0000 Subject: [PATCH 2/3] jsdocify comments in the users subsystem --- packages/instance/src/subsystems/users.ts | 214 +++++++++++++--------- 1 file changed, 132 insertions(+), 82 deletions(-) diff --git a/packages/instance/src/subsystems/users.ts b/packages/instance/src/subsystems/users.ts index a3f4f01..862a9b5 100644 --- a/packages/instance/src/subsystems/users.ts +++ b/packages/instance/src/subsystems/users.ts @@ -28,9 +28,11 @@ export class WorkspacesUser { return path.join(this.instance.subSystems.filesystem.FS_ROOT, `users/${this.userId}`); } - // Sets the user's username to username - // @returns {false} failed to change the username - // @returns {true} successfully changed the username + /** + Sets the user's username to username + @return {false} failed to change the username + @return {true} successfully changed the username + */ async setUsername(username: string): Promise { const db = this.instance.subSystems.database.db(); @@ -45,18 +47,22 @@ export class WorkspacesUser { } } - // Get the user's username - // @returns {string} the users username - // @returns {undefined} could not get the user's username + /** + Get the user's username + @return {string} the users username + @return {undefined} could not get the user's username + */ async getUsername(): Promise { const db = this.instance.subSystems.database.db(); return (await db`SELECT username FROM Users WHERE id = ${this.userId}`)?.[0]?.username || undefined; } - // Sets the user's forename to forename - // @returns {false} failed to change the forename - // @returns {true} successfully changed the forename + /** + Sets the user's forename to forename + @return {false} failed to change the forename + @return {true} successfully changed the forename + */ async setForename(forename: string): Promise { const db = this.instance.subSystems.database.db(); @@ -70,18 +76,22 @@ export class WorkspacesUser { } } - // Get the user's forename - // @returns {string} the users forename - // @returns {undefined} could not get the user's forename + /** + Get the user's forename + @return {string} the users forename + @return {undefined} could not get the user's forename + */ async getForename(): Promise { const db = this.instance.subSystems.database.db(); return (await db`SELECT forename FROM Users WHERE id = ${this.userId}`)?.[0]?.forename || undefined; } - // Sets the user's surname to surname - // @returns {false} failed to change the surname - // @returns {true} successfully changed the surname + /** + Sets the user's surname to surname + @return {false} failed to change the surname + @return {true} successfully changed the surname + */ async setSurname(surname: string): Promise { const db = this.instance.subSystems.database.db(); @@ -95,18 +105,22 @@ export class WorkspacesUser { } } - // Get the user's surname - // @returns {string} the users surname - // @returns {undefined} could not get the user's surname + /** + Get the user's surname + @return {string} the users surname + @return {undefined} could not get the user's surname + */ async getSurname(): Promise { const db = this.instance.subSystems.database.db(); return (await db`SELECT surname FROM Users WHERE id = ${this.userId}`)?.[0]?.surname || undefined; } - // Sets the user's forename and surname to the provided forename and surname - // @returns {false} failed to change the surname - // @returns {true} successfully changed the surname + /** + Sets the user's forename and surname to the provided forename and surname + @return {false} failed to change the surname + @return {true} successfully changed the surname + */ async setFullName(forename: string, surname: string): Promise { let forenameRes = await this.setForename(forename); let surnameRes = await this.setSurname(surname); @@ -114,8 +128,10 @@ export class WorkspacesUser { return forenameRes && surnameRes; } - // Gets the user's forename and surname - // @returns {{ forename?: string, surname?: string }} + /** + Gets the user's forename and surname + @returns {{ forename?: string, surname?: string }} + */ async getFullName(forename: string, surname: string): Promise<{ forename?: string; surname?: string }> { let forenameRes = await this.getForename(); let surnameRes = await this.getSurname(); @@ -123,9 +139,11 @@ export class WorkspacesUser { return { forename: forenameRes, surname: surnameRes }; } - // Sets the user's quota to the provided quota - // @returns {false} failed to set the quota - // @returns {true} successfully set the quota + /** + Sets the user's quota to the provided quota + @returns {false} failed to set the quota + @returns {true} successfully set the quota + */ async setQuota(quota: number): Promise { const db = this.instance.subSystems.database.db(); @@ -138,18 +156,22 @@ export class WorkspacesUser { } } - // Get the user's quota - // @returns {number} the users quota - // @returns {undefined} could not get the user's quota + /** + Get the user's quota + @returns {number} the users quota + @returns {undefined} could not get the user's quota + */ async getQuota(): Promise { const db = this.instance.subSystems.database.db(); return (await db`SELECT storage_quota FROM Users WHERE id = ${this.userId}`)?.[0]?.storage_quota || undefined; } - // Sets the user's bio to bio - // @returns {false} failed to change the bio - // @returns {true} successfully changed the bio + /** + Sets the user's bio to bio + @returns {false} failed to change the bio + @returns {true} successfully changed the bio + */ async setBio(bio: string): Promise { const db = this.instance.subSystems.database.db(); @@ -162,18 +184,22 @@ export class WorkspacesUser { } } - // Get the user's bio - // @returns {string} the users bio - // @returns {undefined} could not get the user's bio + /** + Get the user's bio + @returns {string} the users bio + @returns {undefined} could not get the user's bio + */ async getBio(): Promise { const db = this.instance.subSystems.database.db(); return (await db`SELECT bio FROM Users WHERE id = ${this.userId}`)?.[0]?.bio || undefined; } - // Sets the user's email to email - // @returns {false} failed to change the email - // @returns {true} successfully changed the email + /** + Sets the user's email to email + @returns {false} failed to change the email + @returns {true} successfully changed the email + */ async setEmail(email: string): Promise { const db = this.instance.subSystems.database.db(); try { @@ -185,18 +211,22 @@ export class WorkspacesUser { } } - // Get the user's email - // @returns {string} the users email - // @returns {undefined} could not get the user's email + /** + Get the user's email + @returns {string} the users email + @returns {undefined} could not get the user's email + */ async getEmail(): Promise { const db = this.instance.subSystems.database.db(); return (await db`SELECT email FROM Users WHERE id = ${this.userId}`)?.[0]?.email || undefined; } - // Sets the user's gender to gender - // @returns {false} failed to change the gender - // @returns {true} successfully changed the gender + /** + Sets the user's gender to gender + @returns {false} failed to change the gender + @returns {true} successfully changed the gender + */ async setGender(gender: "female" | "male" | "other"): Promise { const db = this.instance.subSystems.database.db(); @@ -209,18 +239,22 @@ export class WorkspacesUser { } } - // Get the user's gender - // @returns {"female" | "male" | "other"} the users gender - // @returns {undefined} could not get the user's gender + /** + Get the user's gender + @returns {"female" | "male" | "other"} the users gender + @returns {undefined} could not get the user's gender + */ async getGender(): Promise { const db = this.instance.subSystems.database.db(); return (await db`SELECT gender FROM Users WHERE id = ${this.userId}`)?.[0]?.gender || undefined; } - // sets the user's original quality avatar to avatarFile - // @returns {true} - copied the avatarFile successfully - // @returns {false} - failed to copy the avatarFile + /** + sets the user's original quality avatar to avatarFile + @returns {true} - copied the avatarFile successfully + @returns {false} - failed to copy the avatarFile + */ async setAvatar(avatarFile: string): Promise { try { await fs.cp(avatarFile, path.join(this.getPath(), "assets/avatar/avatar.png")); @@ -231,9 +265,11 @@ export class WorkspacesUser { } } - // generates the required avatar sizes from the user's avatarFile, if override is not set to true, only missing avatar sizes will be generated - // @returns {true} - generated required avatar files successfully - // @returns {false} - failed to generate all required avatar files + /** + generates the required avatar sizes from the user's avatarFile, if override is not set to true, only missing avatar sizes will be generated + @returns {true} - generated required avatar files successfully + @returns {false} - failed to generate all required avatar files + */ async generateAvatars(override?: boolean): Promise { const AVATAR_SIZES: { width: number; height: number; name: string }[] = [ { width: 16, height: 16, name: "xs" }, @@ -260,8 +296,10 @@ export class WorkspacesUser { return true; } - // Check that all required directories and assets are valid for this user. - // If they are missing or invalid, directories will be created and assets will be generated / copied + /** + Check that all required directories and assets are valid for this user. + If they are missing or invalid, directories will be created and assets will be generated / copied + */ async verify() { const USER_DIRECTORIES = [ "/", @@ -308,19 +346,21 @@ export default class UsersSubsystem extends SubSystem { const db = this.instance.subSystems.database.db(); - // init the users databasecurrentCommandInterface - // - // id - permanent unique user id number (number) - // username - the user's changable username (string) - // forename - the user's chosen forename (string) - // surname - the user's chosen surname (string) - // gender - the user's chosen gender ("female" | "male" | "other") - // bio - the user's chosen bio (string) - // storage_quota - the user's storage quota in MB (number) - // email - the user's chosen contact email (string) - // is_email_verified - is the user's chosen contact email verified to be theirs (boolean) - // socials - the user's chosen social media links in the format '[name]:-:[url]' as such, the string ':-:' must not be in either [name] or [url] (string[]) - // hashed_password - the user's password after it has been hashed by bun (string) + /** + init the users databasecurrentCommandInterface + + id - permanent unique user id number (number) + username - the user's changable username (string) + forename - the user's chosen forename (string) + surname - the user's chosen surname (string) + gender - the user's chosen gender ("female" | "male" | "other") + bio - the user's chosen bio (string) + storage_quota - the user's storage quota in MB (number) + email - the user's chosen contact email (string) + is_email_verified - is the user's chosen contact email verified to be theirs (boolean) + socials - the user's chosen social media links in the format '[name]:-:[url]' as such, the string ':-:' must not be in either [name] or [url] (string[]) + hashed_password - the user's password after it has been hashed by bun (string) + */ await db`CREATE TABLE IF NOT EXISTS Users ( id SERIAL PRIMARY KEY, username TEXT, @@ -353,9 +393,11 @@ export default class UsersSubsystem extends SubSystem { return true; } - // Create a new Workspaces User - // @returns {number} the userId of the created user - // @returns {undefined} the user already exists + /** + Create a new Workspaces User + @returns {number} the userId of the created user + @returns {undefined} the user already exists + */ async createUser(username: string): Promise { const db = this.instance.subSystems.database.db(); @@ -383,9 +425,11 @@ export default class UsersSubsystem extends SubSystem { return id; } - // Does a user with the userId exist - // @returns {true} they exist - // @returns {false} they do not exist + /** + Does a user with the userId exist + @returns {true} they exist + @returns {false} they do not exist + */ async doesUserExist(userId: number): Promise { const db = this.instance.subSystems.database.db(); @@ -394,26 +438,32 @@ export default class UsersSubsystem extends SubSystem { return false; } - // Gets an array of all users registered on this instance - // @returns {WorkspacesUser[]} an array of all users + /** + Gets an array of all users registered on this instance + @returns {WorkspacesUser[]} an array of all users + */ async getAllUsers(): Promise { const db = this.instance.subSystems.database.db(); return (await db`SELECT id FROM Users`).map((u: { id: number }) => new WorkspacesUser(this.instance, u.id)); } - // Gets a user by their userId - // @returns {WorkspacesUser} the user - // @returns {undefined} no such user exists + /** + Gets a user by their userId + @returns {WorkspacesUser} the user + @returns {undefined} no such user exists + */ async getUserById(userId: number): Promise { if (this.doesUserExist(userId) === undefined) return undefined; return new WorkspacesUser(this.instance, userId); } - // Gets a user by their username - // @returns {WorkspacesUser} the user - // @returns {undefined} no such user exists + /** + Gets a user by their username + @returns {WorkspacesUser} the user + @returns {undefined} no such user exists + */ async getUserByUsername(username: string): Promise { const db = this.instance.subSystems.database.db(); -- 2.51.2 From 09955421afe73d13d25e6dd305ae4d790e03c140 Mon Sep 17 00:00:00 2001 From: ewsgit Date: Mon, 3 Nov 2025 13:55:55 +0000 Subject: [PATCH 3/3] documentation --- README.md | 28 ++++++++++++++-------------- packages/instance/src/index.ts | 1 + 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 32bddc5..3829e40 100644 --- a/README.md +++ b/README.md @@ -45,26 +45,26 @@ A working environment for professionals and Home-Lab users. ### Installation 1. Choose either the `Full-Stack` or `UIKit` section to continue #### Full-Stack (Workspaces) -1. Ensure all non-NPM dependencies are installed before continuing +1. Ensure all non-NPM dependencies are installed + 1. Linux (Ubuntu) + 1. Install PostgreSQL -> `sudo apt install postgresql postgresql-contrib` + 2. Start the PostgreSQL service -> `sudo systemctl start postgresql` + 3. Switch to the postgres user -> `sudo su postgres` + 4. Open the PostgreSQL database with psql -> `psql` + 5. Create a PostgreSQL database with the following query -> `CREATE DATABASE tricolor_workspaces;` + 6. Exit psql -> `exit;` + 7. Logout from the postgres user -> `exit` + 2. Windows + 1. Simply install postgreSQL with the setup file downloaded from the postgreSQL website + 3. MacOS + 1. Using Orbstack with Ubuntu follow the linux instructions above 2. Run `bun install` to install all NPM dependencies 3. Run `bun run web` and `bun run backend` to start up the web interface and backend in development mode 4. Enjoy! :D + #### UIKit (UI Library dev page) 1. Ensure all non-NPM dependencies are installed before continuing 2. Run `bun install` to install all NPM dependencies 3. cd into the UIKit directory `cd ./packages/uikit-solid` 4. Run `bun run dev-web` to start up the web interface and backend in development mode 5. Enjoy! :D - - -# Project Core Functionality Roadmap -- backend - - notifications - - authentication - - filesystem - - users - - teams -- web - - login / signup flow - - Application-Frame layout - - Notification visuals diff --git a/packages/instance/src/index.ts b/packages/instance/src/index.ts index baba707..8e3b51a 100644 --- a/packages/instance/src/index.ts +++ b/packages/instance/src/index.ts @@ -192,6 +192,7 @@ class Instance { process.stdout.write("Shutdown completed! -> "); process.stdout.write(goodbye[Math.floor(Math.random() * goodbye.length)]); + process.stdout.write("\n"); process.exit(0); } } -- 2.51.2