diff --git a/.run/index.ts.run.xml b/.run/index.ts.run.xml
index 06bcec2..33da709 100644
--- a/.run/index.ts.run.xml
+++ b/.run/index.ts.run.xml
@@ -1,7 +1,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 3fd353d..19cf379 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -17,7 +17,7 @@
"request": "launch",
"name": "Launch Program",
"type": "node",
- "program": "${workspaceFolder}/instance/src/index.ts",
+ "program": "${workspaceFolder}/backend/src/index.ts",
"cwd": "${workspaceFolder}/",
"env": {},
"runtimeExecutable": "/home/ewsgit/.deno/bin/deno",
diff --git a/README.md b/README.md
index 5f56f19..92e2df0 100644
--- a/README.md
+++ b/README.md
@@ -71,7 +71,7 @@ A self-hosted web platform for applications & services with design based on Goog
11. run `systemctl enable workspaces-backend --now` to start the backend
12. change into the project root `/var/www/workspaces`
13. run `deno build-web`
-14. choose a webserver of your choice to serve `/var/www/workspaces/instance/web/dist` (caddy is fast & easy to use)
+14. choose a webserver of your choice to serve `/var/www/workspaces/web/dist` (caddy is fast & easy to use)
15. open your browser and head to `https://[server-ip]` and login as the user `admin` with password `password` to finish setup.
-->
diff --git a/instance/.gitignore b/backend/.gitignore
similarity index 100%
rename from instance/.gitignore
rename to backend/.gitignore
diff --git a/instance/deno.json b/backend/deno.json
similarity index 67%
rename from instance/deno.json
rename to backend/deno.json
index 8b55534..e097df6 100644
--- a/instance/deno.json
+++ b/backend/deno.json
@@ -3,7 +3,8 @@
"@std/fs": "jsr:@std/fs@^1.0.24",
"@std/http": "jsr:@std/http@^1.1.1",
"@std/path": "jsr:@std/path@^1.1.5",
- "@opentui/core": "npm:@opentui/core@^0.4.1"
+ "@opentui/core": "npm:@opentui/core@^0.4.1",
+ "@napi-rs/canvas": "npm:@napi-rs/canvas@^1.0.0"
},
"permissions": {
"default": {
diff --git a/instance/meta/backend.service b/backend/meta/backend.service
similarity index 85%
rename from instance/meta/backend.service
rename to backend/meta/backend.service
index 98337b9..b566687 100644
--- a/instance/meta/backend.service
+++ b/backend/meta/backend.service
@@ -6,7 +6,7 @@ StartLimitIntervalSec=60
[Service]
Type=simple
-ExecStart=/var/www/workspaces/instance/meta/backend.sh
+ExecStart=/var/www/workspaces/backend/meta/backend.sh
Restart=on-failure
RestartSec=5
User=root
diff --git a/instance/meta/backend.sh b/backend/meta/backend.sh
similarity index 80%
rename from instance/meta/backend.sh
rename to backend/meta/backend.sh
index cd7a358..d61a03c 100755
--- a/instance/meta/backend.sh
+++ b/backend/meta/backend.sh
@@ -5,4 +5,4 @@ cd /var/www/workspaces/ || {
exit 1
}
-bun ./instance/src/index.ts
+bun ./backend/src/index.ts
diff --git a/instance/package.json b/backend/package.json
similarity index 97%
rename from instance/package.json
rename to backend/package.json
index c4706e5..2f3e859 100644
--- a/instance/package.json
+++ b/backend/package.json
@@ -22,7 +22,6 @@
"@types/ws": "^8.18.1",
"@types/yargs": "17.0.35",
"ansi-escapes": "^7.2.0",
- "canvas": "^3.2.3",
"chalk": "^5.5.0",
"cookies": "^0.9.1",
"cors": "^2.8.5",
diff --git a/instance/src/assets/fs_template_files/markdown.md b/backend/src/assets/fs_template_files/markdown.md
similarity index 100%
rename from instance/src/assets/fs_template_files/markdown.md
rename to backend/src/assets/fs_template_files/markdown.md
diff --git a/instance/src/assets/fs_template_files/plaintext.txt b/backend/src/assets/fs_template_files/plaintext.txt
similarity index 100%
rename from instance/src/assets/fs_template_files/plaintext.txt
rename to backend/src/assets/fs_template_files/plaintext.txt
diff --git a/instance/src/assets/placeholder/avatar.png b/backend/src/assets/placeholder/avatar.png
similarity index 100%
rename from instance/src/assets/placeholder/avatar.png
rename to backend/src/assets/placeholder/avatar.png
diff --git a/instance/src/assets/placeholder/banner.png b/backend/src/assets/placeholder/banner.png
similarity index 100%
rename from instance/src/assets/placeholder/banner.png
rename to backend/src/assets/placeholder/banner.png
diff --git a/instance/src/assets/placeholder/file.png b/backend/src/assets/placeholder/file.png
similarity index 100%
rename from instance/src/assets/placeholder/file.png
rename to backend/src/assets/placeholder/file.png
diff --git a/instance/src/assets/wallpapers/generic_background.png b/backend/src/assets/wallpapers/generic_background.png
similarity index 100%
rename from instance/src/assets/wallpapers/generic_background.png
rename to backend/src/assets/wallpapers/generic_background.png
diff --git a/instance/src/assets/wallpapers/pexels-argoallvee-27976588.jpg b/backend/src/assets/wallpapers/pexels-argoallvee-27976588.jpg
similarity index 100%
rename from instance/src/assets/wallpapers/pexels-argoallvee-27976588.jpg
rename to backend/src/assets/wallpapers/pexels-argoallvee-27976588.jpg
diff --git a/instance/src/assets/wallpapers/pexels-eberhardgross-1624496.jpg b/backend/src/assets/wallpapers/pexels-eberhardgross-1624496.jpg
similarity index 100%
rename from instance/src/assets/wallpapers/pexels-eberhardgross-1624496.jpg
rename to backend/src/assets/wallpapers/pexels-eberhardgross-1624496.jpg
diff --git a/instance/src/assets/wallpapers/pexels-jackerkun-10428721.jpg b/backend/src/assets/wallpapers/pexels-jackerkun-10428721.jpg
similarity index 100%
rename from instance/src/assets/wallpapers/pexels-jackerkun-10428721.jpg
rename to backend/src/assets/wallpapers/pexels-jackerkun-10428721.jpg
diff --git a/instance/src/assets/wallpapers/pexels-jackerkun-10428723.jpg b/backend/src/assets/wallpapers/pexels-jackerkun-10428723.jpg
similarity index 100%
rename from instance/src/assets/wallpapers/pexels-jackerkun-10428723.jpg
rename to backend/src/assets/wallpapers/pexels-jackerkun-10428723.jpg
diff --git a/instance/src/assets/wallpapers/pexels-jackerkun-12126032.jpg b/backend/src/assets/wallpapers/pexels-jackerkun-12126032.jpg
similarity index 100%
rename from instance/src/assets/wallpapers/pexels-jackerkun-12126032.jpg
rename to backend/src/assets/wallpapers/pexels-jackerkun-12126032.jpg
diff --git a/instance/src/assets/wallpapers/pexels-steve-29708303.jpg b/backend/src/assets/wallpapers/pexels-steve-29708303.jpg
similarity index 100%
rename from instance/src/assets/wallpapers/pexels-steve-29708303.jpg
rename to backend/src/assets/wallpapers/pexels-steve-29708303.jpg
diff --git a/instance/src/assets/wallpapers/pexels-tina-p-891488179-33017133.jpg b/backend/src/assets/wallpapers/pexels-tina-p-891488179-33017133.jpg
similarity index 100%
rename from instance/src/assets/wallpapers/pexels-tina-p-891488179-33017133.jpg
rename to backend/src/assets/wallpapers/pexels-tina-p-891488179-33017133.jpg
diff --git a/instance/src/assets/wallpapers/stars_wallpaper.png b/backend/src/assets/wallpapers/stars_wallpaper.png
similarity index 100%
rename from instance/src/assets/wallpapers/stars_wallpaper.png
rename to backend/src/assets/wallpapers/stars_wallpaper.png
diff --git a/instance/src/index.ts b/backend/src/index.ts
similarity index 92%
rename from instance/src/index.ts
rename to backend/src/index.ts
index 14a151a..280a72b 100644
--- a/instance/src/index.ts
+++ b/backend/src/index.ts
@@ -19,6 +19,8 @@ import TRPCSystem from "./systems/trpc.ts";
import { createTRPCContext as createWorkspacesTRPCContext, workspacesRouter } from "./systems/trpcRouter.ts";
import UsersSystem from "./systems/users.ts";
import WebFrontendSystem from "./systems/webFrontend.ts";
+import { routeRadix } from "@std/http/unstable-route";
+import ApiSystem from "./systems/api.ts";
export enum InstanceStatus {
Online,
@@ -57,6 +59,7 @@ class Instance {
this.sys.email = new EmailSystem(this);
this.sys.reverseProxy = new ReverseProxySystem(this);
this.sys.terminal = new TerminalUISystem(this);
+ this.sys.api = new ApiSystem(this);
this.status = InstanceStatus.Offline;
}
@@ -91,18 +94,10 @@ class Instance {
this.sys.event.invoke(WorkspacesEvent.BeforeStartupComplete);
- // this.webServer = Deno.serve(route(, () => {})
- // // this.sys.tRPC.serve({
- // // routes: {
-
- // // },
- // // fetch() {
- // // // will be executed if it's not a TRPC request
- // // return new Response("Unknown path");
- // // },
- // // development: this.sys.configuration.isDevMode,
- // // }),
- // );
+ this.webServer = Deno.serve(
+ { port: this.sys.configuration.apiPort },
+ routeRadix(this.sys.api.routes, () => Response.json({ failed: true })),
+ );
this.sys.tRPC.registeredRouters.push({
basePath: "/api/trpc",
diff --git a/instance/src/log.ts b/backend/src/log.ts
similarity index 100%
rename from instance/src/log.ts
rename to backend/src/log.ts
diff --git a/instance/src/system.ts b/backend/src/system.ts
similarity index 97%
rename from instance/src/system.ts
rename to backend/src/system.ts
index 4caef63..4f20e8e 100644
--- a/instance/src/system.ts
+++ b/backend/src/system.ts
@@ -1,5 +1,6 @@
import type { Instance } from "./index.ts";
import type { Logger } from "./log.ts";
+import ApiSystem from "./systems/api.ts";
import type ApplicationsSubsystem from "./systems/applications.ts";
import type AuthorizationSubsystem from "./systems/authorization.ts";
import type ConfigurationSubsystem from "./systems/configuration.ts";
@@ -34,6 +35,7 @@ export type Sys = {
event: EventSystem;
reverseProxy: ReverseProxySystem;
terminal: TerminalSystem;
+ api: ApiSystem;
} & { [key: string]: System };
export default abstract class System {
@@ -48,7 +50,6 @@ export default abstract class System {
}
async startup(): Promise {
- this.log.info("Starting up...");
return true;
}
diff --git a/instance/src/systems/api.ts b/backend/src/systems/api.ts
similarity index 86%
rename from instance/src/systems/api.ts
rename to backend/src/systems/api.ts
index 8c5c5ea..0a5524c 100644
--- a/instance/src/systems/api.ts
+++ b/backend/src/systems/api.ts
@@ -1,274 +1,263 @@
-import { serveFile } from "@std/http/file-server";
-import type { Route } from "@std/http/unstable-route";
-import * as path from "@std/path/posix";
-import nodeCanvas from "canvas";
-import type { Instance } from "../index.ts";
-import System from "../system.ts";
-
-export default class ApiSystem extends System {
- private routes: Route[];
-
- constructor(instance: Instance) {
- super("api", instance);
-
- // deno-lint-ignore no-this-alias
- const self = this;
-
- this.routes = [
- {
- method: ["GET"],
- pattern: new URLPattern({ pathname: "/api/instance/login/banner" }),
- handler(req) {
- return serveFile(req, path.join(self.instance.sys.filesystem.FS_ROOT, "assets/login/banner.png"));
- },
- },
- {
- method: ["GET"],
- pattern: new URLPattern({ pathname: "/api/instance/login/background" }),
- handler(req) {
- return serveFile(req, path.join(self.instance.sys.filesystem.FS_ROOT, "assets/login/background.png"));
- },
- },
- {
- method: ["GET"],
- pattern: new URLPattern({ pathname: "/api/user/me/avatar/:size" }),
- async handler(req, _, rawParams) {
- // @ts-ignore this does exist
- const params = rawParams?.pathname.groups;
-
- const size = params.size;
-
- const cookieString = req.headers?.get("cookie");
-
- if (cookieString === null) {
- return Response.json({
- code: "UNAUTHORIZED",
- message: "missing auth cookie",
- });
- }
-
- const parsedCookie = Bun.Cookie.parse(cookieString);
-
- const userId = await self.instance.sys.authorization.verifySession(decodeURIComponent(parsedCookie.value));
-
- if (userId === undefined) {
- return Response.json({
- code: "UNAUTHORIZED",
- message: "invalid session",
- });
- }
-
- switch (size) {
- case "xs":
- case "s":
- case "m":
- case "l":
- case "xl":
- case "2xl":
- return serveFile(req, path.join(self.instance.sys.filesystem.FS_ROOT, `users/${userId}/assets/avatar/${size}.webp`));
- default:
- return serveFile(req, path.join(self.instance.sys.filesystem.FS_ROOT, `users/${userId}/assets/avatar/xs.webp`));
- }
- },
- },
- {
- method: ["GET"],
- pattern: new URLPattern({ pathname: "/api/application/:app/icon/" }),
- async handler(req, _, rawParams) {
- // @ts-ignore this does exist
- const params = rawParams?.pathname.groups;
-
- const app = (params as { app: string }).app;
-
- const cookieString = req.headers?.get("cookie");
-
- if (cookieString === null) {
- self.log.warning("Missing auth cookie in request for application icon");
-
- return Response.json({
- code: "UNAUTHORIZED",
- message: "missing auth cookie",
- });
- }
-
- const parsedCookie = Bun.Cookie.parse(cookieString);
-
- const userId = await self.instance.sys.authorization.verifySession(decodeURIComponent(parsedCookie.value));
-
- if (userId === undefined) {
- self.log.warning("Invalid session in request for application icon");
-
- return Response.json({
- code: "UNAUTHORIZED",
- message: "invalid session",
- });
- }
-
- const application = self.instance.sys.applications.availableApplications.find((a) => a.manifest?.id === app);
-
- if (!application)
- return Response.json({
- code: "INTERNAL_ERROR",
- message: "Invalid application!",
- });
-
- const applicationIconPath = path.join(application.path, application.manifest?.icon?.value || "");
-
- return serveFile(req, applicationIconPath);
- },
- },
- {
- method: ["GET"],
- pattern: new URLPattern({ pathname: "/api/asset/image/:imageId/:resolution" }),
- async handler(req, _, rawParams) {
- // @ts-ignore this does exist
- const params = rawParams?.pathname.groups;
-
- const image = self.instance.sys.image._internalImages.get(params.imageId as string);
-
- if (!image) {
- return new Response("Invalid image");
- }
-
- if (!image.public) {
- const cookieString = req.headers?.get("cookie");
-
- if (cookieString === null) {
- return Response.json({
- code: "UNAUTHORIZED",
- message: "missing auth cookie",
- });
- }
-
- const parsedCookie = Bun.Cookie.parse(cookieString);
-
- const userId = await self.instance.sys.authorization.verifySession(decodeURIComponent(parsedCookie.value));
-
- if (userId === undefined) {
- return Response.json({
- code: "UNAUTHORIZED",
- message: "invalid session",
- });
- }
- }
-
- const resolutionParam = params.resolution as string;
-
- const sourceImage = image[resolutionParam];
-
- if (!sourceImage) {
- return Response.json({
- code: "NOT_FOUND",
- message: "missing image",
- });
- }
-
- if (resolutionParam === "raw") {
- self.instance.sys.image.log.info(`Served Image -> '${(params as { imageId: string }).imageId} @ ${resolutionParam}'`);
- return serveFile(req, sourceImage.path);
- }
-
- return Response.json({ false: true });
- /* TODO: implement hashing again... else {
- const cachedFilePath = path.join(self.instance.sys.filesystem.CACHE_PATH, sourceImage.path.replaceAll(":", ""));
- const outputPath = path.join(cachedFilePath, resolutionParam);
- const hashPath = path.join(`${outputPath}.hash`);
-
- if (fs.existsSync(outputPath)) {
- const fileHash = Bun.hash.rapidhash(await Deno.readFile(sourceImage.path)).toString();
- const cacheFileHash = (await Deno.readFile(hashPath)).toString();
-
- if (fileHash === cacheFileHash) {
- self.instance.sys.image.log.info(`Served Image -> '${(params as { imageId: string }).imageId} @ ${resolutionParam}'`);
- return serveFile(req, outputPath);
- }
- }
-
- if (!fs.existsSync(path.join(outputPath, ".."))) {
- await fs.ensureDir(path.join(outputPath, ".."));
- }
-
- const fileHash = Bun.hash.rapidhash(await Deno.readFile(sourceImage.path)).toString();
- await self.instance.sys.image.resizeImage(sourceImage.path, outputPath, sourceImage.resize!.dimensions, sourceImage.resize!);
- await Deno.writeFile(hashPath, Buffer.from(fileHash));
-
- self.instance.sys.image.log.info(`Served Image -> '${(params as { imageId: string }).imageId} @ ${resolutionParam}'`);
- return serveFile(req, outputPath);
- } */
- },
- },
- {
- method: ["GET"],
- pattern: new URLPattern({ pathname: "/api/asset/raw/:assetId" }),
- async handler(req, _, rawParams) {
- // @ts-ignore this does exist
- const params = rawParams?.pathname.groups;
-
- const asset = self.instance.sys.filesystem._internalAssets.get(params.assetId as string);
-
- if (!asset) {
- return new Response("Invalid raw asset");
- }
-
- if (!asset.public) {
- const cookieString = req.headers?.get("cookie");
-
- if (cookieString === null) {
- return Response.json({
- code: "UNAUTHORIZED",
- message: "missing auth cookie",
- });
- }
-
- const parsedCookie = Bun.Cookie.parse(cookieString);
-
- const userId = await self.instance.sys.authorization.verifySession(decodeURIComponent(parsedCookie.value));
-
- if (userId === undefined) {
- return Response.json({
- code: "UNAUTHORIZED",
- message: "invalid session",
- });
- }
- }
-
- return serveFile(req, asset.path);
- },
- },
- {
- method: ["GET"],
- pattern: new URLPattern({ pathname: "/api/asset/fileicon/:filetype" }),
- async handler(_req, _, rawParams) {
- // @ts-ignore this does exist
- const params = rawParams?.pathname.groups;
-
- const CANVAS_PADDING = 8;
- const CANVAS_WIDTH = 128;
- const CANVAS_HEIGHT = 128;
- const canvas = nodeCanvas.createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
- const ctx = canvas.getContext("2d");
- ctx.drawImage(await nodeCanvas.loadImage(path.join(self.instance.sys.filesystem.SRC_ROOT, "assets/placeholder/file.png")), 0, 0);
-
- ctx.font = `20px Arial`;
- ctx.textAlign = "end";
- ctx.textBaseline = "bottom";
- const LABEL_PADDING: { x: number; y: number } = { x: 4, y: 0 };
- const labelBr: { x: number; y: number } = { x: CANVAS_WIDTH - CANVAS_PADDING, y: CANVAS_HEIGHT - CANVAS_PADDING };
- const textContent = `${params.filetype.toUpperCase().slice(0, 3)}`;
- const textSize = ctx.measureText(textContent);
- ctx.fillStyle = "#00aa00";
- ctx.fillRect(
- labelBr.x - (textSize.width + LABEL_PADDING.x * 2),
- labelBr.y - (textSize.emHeightAscent + LABEL_PADDING.y * 2),
- textSize.width + LABEL_PADDING.x * 2,
- textSize.emHeightAscent + LABEL_PADDING.y * 2,
- );
- ctx.fillStyle = "white";
- ctx.fillText(textContent, labelBr.x - LABEL_PADDING.x, labelBr.y + LABEL_PADDING.y);
-
- return new Response(canvas.createPNGStream());
- },
- },
- ];
- }
-}
+import { serveFile } from "@std/http/file-server";
+import type { Route } from "@std/http/unstable-route";
+import * as path from "@std/path/posix";
+import nodeCanvas from "@napi-rs/canvas";
+import type { Instance } from "../index.ts";
+import System from "../system.ts";
+import { getCookies } from "@std/http";
+
+export default class ApiSystem extends System {
+ routes: Route[];
+
+ constructor(instance: Instance) {
+ super("api", instance);
+
+ // deno-lint-ignore no-this-alias
+ const self = this;
+
+ this.routes = [
+ {
+ method: ["GET"],
+ pattern: new URLPattern({ pathname: "/api/instance/login/banner" }),
+ handler(req) {
+ return serveFile(req, path.join(self.instance.sys.filesystem.FS_ROOT, "assets/login/banner.png"));
+ },
+ },
+ {
+ method: ["GET"],
+ pattern: new URLPattern({ pathname: "/api/instance/login/background" }),
+ handler(req) {
+ return serveFile(req, path.join(self.instance.sys.filesystem.FS_ROOT, "assets/login/background.png"));
+ },
+ },
+ {
+ method: ["GET"],
+ pattern: new URLPattern({ pathname: "/api/user/me/avatar/:size" }),
+ async handler(req, _, rawParams) {
+ // @ts-ignore this does exist
+ const params = rawParams?.pathname.groups;
+
+ const size = params.size;
+
+ const cookies = getCookies(req.headers);
+
+ if (!cookies.Authorization) {
+ return Response.json({
+ code: "UNAUTHORIZED",
+ message: "missing auth cookie",
+ });
+ }
+
+ const userId = await self.instance.sys.authorization.verifySession(decodeURIComponent(cookies.Authorization!));
+
+ if (userId === undefined) {
+ return Response.json({
+ code: "UNAUTHORIZED",
+ message: "invalid session",
+ });
+ }
+
+ switch (size) {
+ case "xs":
+ case "s":
+ case "m":
+ case "l":
+ case "xl":
+ case "2xl":
+ return serveFile(req, path.join(self.instance.sys.filesystem.FS_ROOT, `users/${userId}/assets/avatar/${size}.webp`));
+ default:
+ return serveFile(req, path.join(self.instance.sys.filesystem.FS_ROOT, `users/${userId}/assets/avatar/xs.webp`));
+ }
+ },
+ },
+ {
+ method: ["GET"],
+ pattern: new URLPattern({ pathname: "/api/application/:app/icon/" }),
+ async handler(req, _, rawParams) {
+ // @ts-ignore this does exist
+ const params = rawParams?.pathname.groups;
+
+ const app = (params as { app: string }).app;
+
+ const cookies = getCookies(req.headers);
+
+ if (!cookies.Authorization) {
+ return Response.json({
+ code: "UNAUTHORIZED",
+ message: "missing auth cookie",
+ });
+ }
+
+ const userId = await self.instance.sys.authorization.verifySession(decodeURIComponent(cookies.Authorization!));
+
+ if (userId === undefined) {
+ return Response.json({
+ code: "UNAUTHORIZED",
+ message: "invalid session",
+ });
+ }
+
+ const application = self.instance.sys.applications.availableApplications.find((a) => a.manifest?.id === app);
+
+ if (!application)
+ return Response.json({
+ code: "INTERNAL_ERROR",
+ message: "Invalid application!",
+ });
+
+ const applicationIconPath = path.join(application.path, application.manifest?.icon?.value || "");
+
+ return serveFile(req, applicationIconPath);
+ },
+ },
+ {
+ method: ["GET"],
+ pattern: new URLPattern({ pathname: "/api/asset/image/:imageId/:resolution" }),
+ async handler(req, _, rawParams) {
+ // @ts-ignore this does exist
+ const params = rawParams?.pathname.groups;
+
+ const image = self.instance.sys.image._internalImages.get(params.imageId as string);
+
+ if (!image) {
+ return new Response("Invalid image");
+ }
+
+ if (!image.public) {
+ const cookies = getCookies(req.headers);
+
+ if (!cookies.Authorization) {
+ return Response.json({
+ code: "UNAUTHORIZED",
+ message: "missing auth cookie",
+ });
+ }
+
+ const userId = await self.instance.sys.authorization.verifySession(decodeURIComponent(cookies.Authorization!));
+
+ if (userId === undefined) {
+ return Response.json({
+ code: "UNAUTHORIZED",
+ message: "invalid session",
+ });
+ }
+ }
+
+ const resolutionParam = params.resolution as string;
+
+ const sourceImage = image[resolutionParam];
+
+ if (!sourceImage) {
+ return Response.json({
+ code: "NOT_FOUND",
+ message: "missing image",
+ });
+ }
+
+ if (resolutionParam === "raw") {
+ self.instance.sys.image.log.info(`Served Image -> '${(params as { imageId: string }).imageId} @ ${resolutionParam}'`);
+ return serveFile(req, sourceImage.path);
+ }
+
+ return Response.json({ false: true });
+ /* TODO: implement hashing again... else {
+ const cachedFilePath = path.join(self.instance.sys.filesystem.CACHE_PATH, sourceImage.path.replaceAll(":", ""));
+ const outputPath = path.join(cachedFilePath, resolutionParam);
+ const hashPath = path.join(`${outputPath}.hash`);
+
+ if (fs.existsSync(outputPath)) {
+ const fileHash = Bun.hash.rapidhash(await Deno.readFile(sourceImage.path)).toString();
+ const cacheFileHash = (await Deno.readFile(hashPath)).toString();
+
+ if (fileHash === cacheFileHash) {
+ self.instance.sys.image.log.info(`Served Image -> '${(params as { imageId: string }).imageId} @ ${resolutionParam}'`);
+ return serveFile(req, outputPath);
+ }
+ }
+
+ if (!fs.existsSync(path.join(outputPath, ".."))) {
+ await fs.ensureDir(path.join(outputPath, ".."));
+ }
+
+ const fileHash = Bun.hash.rapidhash(await Deno.readFile(sourceImage.path)).toString();
+ await self.instance.sys.image.resizeImage(sourceImage.path, outputPath, sourceImage.resize!.dimensions, sourceImage.resize!);
+ await Deno.writeFile(hashPath, Buffer.from(fileHash));
+
+ self.instance.sys.image.log.info(`Served Image -> '${(params as { imageId: string }).imageId} @ ${resolutionParam}'`);
+ return serveFile(req, outputPath);
+ } */
+ },
+ },
+ {
+ method: ["GET"],
+ pattern: new URLPattern({ pathname: "/api/asset/raw/:assetId" }),
+ async handler(req, _, rawParams) {
+ // @ts-ignore this does exist
+ const params = rawParams?.pathname.groups;
+
+ const asset = self.instance.sys.filesystem._internalAssets.get(params.assetId as string);
+
+ if (!asset) {
+ return new Response("Invalid raw asset");
+ }
+
+ if (!asset.public) {
+ const cookies = getCookies(req.headers);
+
+ if (!cookies.Authorization) {
+ return Response.json({
+ code: "UNAUTHORIZED",
+ message: "missing auth cookie",
+ });
+ }
+
+ const userId = await self.instance.sys.authorization.verifySession(decodeURIComponent(cookies.Authorization!));
+
+ if (userId === undefined) {
+ return Response.json({
+ code: "UNAUTHORIZED",
+ message: "invalid session",
+ });
+ }
+ }
+
+ return serveFile(req, asset.path);
+ },
+ },
+ {
+ method: ["GET"],
+ pattern: new URLPattern({ pathname: "/api/asset/fileicon/:filetype" }),
+ async handler(_req, _, rawParams) {
+ // @ts-ignore this does exist
+ const params = rawParams?.pathname.groups;
+
+ const CANVAS_PADDING = 8;
+ const CANVAS_WIDTH = 128;
+ const CANVAS_HEIGHT = 128;
+ const canvas = nodeCanvas.createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(await nodeCanvas.loadImage(path.join(self.instance.sys.filesystem.SRC_ROOT, "assets/placeholder/file.png")), 0, 0);
+
+ ctx.font = `20px Arial`;
+ ctx.textAlign = "end";
+ ctx.textBaseline = "bottom";
+ const LABEL_PADDING: { x: number; y: number } = { x: 4, y: 0 };
+ const labelBr: { x: number; y: number } = { x: CANVAS_WIDTH - CANVAS_PADDING, y: CANVAS_HEIGHT - CANVAS_PADDING };
+ const textContent = `${params.filetype.toUpperCase().slice(0, 3)}`;
+ const textSize = ctx.measureText(textContent);
+ ctx.fillStyle = "#00aa00";
+ ctx.fillRect(
+ labelBr.x - (textSize.width + LABEL_PADDING.x * 2),
+ labelBr.y - (textSize.emHeightAscent + LABEL_PADDING.y * 2),
+ textSize.width + LABEL_PADDING.x * 2,
+ textSize.emHeightAscent + LABEL_PADDING.y * 2,
+ );
+ ctx.fillStyle = "white";
+ ctx.fillText(textContent, labelBr.x - LABEL_PADDING.x, labelBr.y + LABEL_PADDING.y);
+
+ return new Response(canvas.createPNGStream());
+ },
+ },
+ ];
+ }
+}
diff --git a/instance/src/systems/applications.ts b/backend/src/systems/applications.ts
similarity index 100%
rename from instance/src/systems/applications.ts
rename to backend/src/systems/applications.ts
diff --git a/instance/src/systems/applications/application.ts b/backend/src/systems/applications/application.ts
similarity index 100%
rename from instance/src/systems/applications/application.ts
rename to backend/src/systems/applications/application.ts
diff --git a/instance/src/systems/applications/serviceStatus.ts b/backend/src/systems/applications/serviceStatus.ts
similarity index 100%
rename from instance/src/systems/applications/serviceStatus.ts
rename to backend/src/systems/applications/serviceStatus.ts
diff --git a/instance/src/systems/authorization.ts b/backend/src/systems/authorization.ts
similarity index 100%
rename from instance/src/systems/authorization.ts
rename to backend/src/systems/authorization.ts
diff --git a/instance/src/systems/commandBackup/application.ts b/backend/src/systems/commandBackup/application.ts
similarity index 100%
rename from instance/src/systems/commandBackup/application.ts
rename to backend/src/systems/commandBackup/application.ts
diff --git a/instance/src/systems/commandBackup/feature.ts b/backend/src/systems/commandBackup/feature.ts
similarity index 100%
rename from instance/src/systems/commandBackup/feature.ts
rename to backend/src/systems/commandBackup/feature.ts
diff --git a/instance/src/systems/commandBackup/install_application.ts b/backend/src/systems/commandBackup/install_application.ts
similarity index 100%
rename from instance/src/systems/commandBackup/install_application.ts
rename to backend/src/systems/commandBackup/install_application.ts
diff --git a/instance/src/systems/commandBackup/notify.ts b/backend/src/systems/commandBackup/notify.ts
similarity index 100%
rename from instance/src/systems/commandBackup/notify.ts
rename to backend/src/systems/commandBackup/notify.ts
diff --git a/instance/src/systems/commandBackup/op.ts b/backend/src/systems/commandBackup/op.ts
similarity index 100%
rename from instance/src/systems/commandBackup/op.ts
rename to backend/src/systems/commandBackup/op.ts
diff --git a/instance/src/systems/commandBackup/password.ts b/backend/src/systems/commandBackup/password.ts
similarity index 100%
rename from instance/src/systems/commandBackup/password.ts
rename to backend/src/systems/commandBackup/password.ts
diff --git a/instance/src/systems/commandBackup/restart.ts b/backend/src/systems/commandBackup/restart.ts
similarity index 100%
rename from instance/src/systems/commandBackup/restart.ts
rename to backend/src/systems/commandBackup/restart.ts
diff --git a/instance/src/systems/commandBackup/stop.ts b/backend/src/systems/commandBackup/stop.ts
similarity index 100%
rename from instance/src/systems/commandBackup/stop.ts
rename to backend/src/systems/commandBackup/stop.ts
diff --git a/instance/src/systems/commandBackup/useradd.ts b/backend/src/systems/commandBackup/useradd.ts
similarity index 100%
rename from instance/src/systems/commandBackup/useradd.ts
rename to backend/src/systems/commandBackup/useradd.ts
diff --git a/instance/src/systems/commandBackup/userdel.ts b/backend/src/systems/commandBackup/userdel.ts
similarity index 100%
rename from instance/src/systems/commandBackup/userdel.ts
rename to backend/src/systems/commandBackup/userdel.ts
diff --git a/instance/src/systems/commandBackup/users.ts b/backend/src/systems/commandBackup/users.ts
similarity index 100%
rename from instance/src/systems/commandBackup/users.ts
rename to backend/src/systems/commandBackup/users.ts
diff --git a/instance/src/systems/configuration.ts b/backend/src/systems/configuration.ts
similarity index 89%
rename from instance/src/systems/configuration.ts
rename to backend/src/systems/configuration.ts
index 4545369..98fd839 100644
--- a/instance/src/systems/configuration.ts
+++ b/backend/src/systems/configuration.ts
@@ -27,6 +27,14 @@ export default class ConfigurationSystem extends System {
port: number;
database: string;
};
+ } = {
+ postgres: {
+ user: "postgres",
+ password: "postgres",
+ host: "localhost",
+ port: 5432,
+ database: "onlineworkspace_workspace",
+ },
};
proxyUrl: string;
enabledFeatures: (WorkspacesFeatureFlags | string)[];
@@ -40,6 +48,16 @@ export default class ConfigurationSystem extends System {
minimumNumbers?: number;
minimumSymbols?: number;
};
+ } = {
+ email: false,
+ twoFactorAuthentication: false,
+ passwordMinimumLength: 5,
+ passwordContains: {
+ minimumLowercase: 1,
+ minimumNumbers: 1,
+ minimumSymbols: 1,
+ minimumUppercase: 1,
+ },
};
displayName: string;
mailServer: {
@@ -52,36 +70,26 @@ export default class ConfigurationSystem extends System {
};
};
termsOfUse: { message: string; lastUpdated: number };
- defaultQuickShortcuts: string[];
+ defaultQuickShortcuts: string[] = ["uk.ewsgit.dashboard", "uk.ewsgit.store", "uk.ewsgit.settings", "uk.ewsgit.photos", "uk.ewsgit.files"];
defaultApplications: { id: string; uri: string }[];
+ userDefault: {
+ homeDirectories: string[];
+ quotaSize: number;
+ displayNameFormat: string;
+ } = {
+ homeDirectories: ["Documents", "Photos", "Videos", "Projects"],
+ quotaSize: 1024 * 1024 * 10,
+ displayNameFormat: "New User %num%",
+ };
caddyfile: string | undefined;
+ apiPort: number = 3563;
constructor(instance: Instance) {
super("configuration", instance);
this.enabledFeatures = [WorkspacesFeatureFlags.SlashCommands];
- this.databases = {
- postgres: {
- user: "postgres",
- password: "postgres",
- host: "localhost",
- port: 5432,
- database: "onlineworkspace_workspace",
- },
- };
if (process.env.POSTGRES_DATABASE_HOST) this.databases.postgres.host = process.env.POSTGRES_DATABASE_HOST;
this.proxyUrl = "https://localhost";
- this.signupRequirements = {
- email: false,
- twoFactorAuthentication: false,
- passwordMinimumLength: 5,
- passwordContains: {
- minimumLowercase: 1,
- minimumNumbers: 1,
- minimumSymbols: 1,
- minimumUppercase: 1,
- },
- };
this.displayName = "Workspace";
this.mailServer = {
host: "smtp.example.com",
@@ -113,7 +121,6 @@ export default class ConfigurationSystem extends System {
- These terms may change. If we make significant updates, we will post a notification within the app or send an email.`,
lastUpdated: Date.now(),
};
- this.defaultQuickShortcuts = ["uk.ewsgit.dashboard", "uk.ewsgit.store", "uk.ewsgit.settings", "uk.ewsgit.photos", "uk.ewsgit.files"];
this.defaultApplications = [
{ id: "uk.ewsgit.dashboard", uri: "local:uk.ewsgit.dashboard" },
{ id: "uk.ewsgit.store", uri: "local:uk.ewsgit.store" },
diff --git a/instance/src/systems/consoleCommands.ts b/backend/src/systems/consoleCommands.ts
similarity index 100%
rename from instance/src/systems/consoleCommands.ts
rename to backend/src/systems/consoleCommands.ts
diff --git a/instance/src/systems/consoleCommands/application.ts b/backend/src/systems/consoleCommands/application.ts
similarity index 100%
rename from instance/src/systems/consoleCommands/application.ts
rename to backend/src/systems/consoleCommands/application.ts
diff --git a/instance/src/systems/consoleCommands/exit.ts b/backend/src/systems/consoleCommands/exit.ts
similarity index 100%
rename from instance/src/systems/consoleCommands/exit.ts
rename to backend/src/systems/consoleCommands/exit.ts
diff --git a/instance/src/systems/database.ts b/backend/src/systems/database.ts
similarity index 100%
rename from instance/src/systems/database.ts
rename to backend/src/systems/database.ts
diff --git a/instance/src/systems/email.ts b/backend/src/systems/email.ts
similarity index 100%
rename from instance/src/systems/email.ts
rename to backend/src/systems/email.ts
diff --git a/instance/src/systems/events.ts b/backend/src/systems/events.ts
similarity index 100%
rename from instance/src/systems/events.ts
rename to backend/src/systems/events.ts
diff --git a/instance/src/systems/filesystem.ts b/backend/src/systems/filesystem.ts
similarity index 96%
rename from instance/src/systems/filesystem.ts
rename to backend/src/systems/filesystem.ts
index ab55dd5..4d9c667 100644
--- a/instance/src/systems/filesystem.ts
+++ b/backend/src/systems/filesystem.ts
@@ -1,241 +1,241 @@
-import fs from "node:fs";
-import path from "node:path";
-import type { Instance } from "../index.ts";
-import System from "../system.ts";
-import { WorkspacesEvent } from "./events.ts";
-
-export default class FilesystemSystem extends System {
- readonly SRC_ROOT = path.resolve("./instance/src/");
- readonly FS_ROOT = path.resolve("./fs/");
- readonly CACHE_PATH = path.join(this.FS_ROOT, "cache");
- readonly SYSTEM_PATH = path.join(this.FS_ROOT, "system");
- readonly AUTOINSTALL_PATH = path.join(process.cwd(), "autoinstall");
-
- _internalAssets: Map;
- _internalAssetPaths: Map;
-
- constructor(instance: Instance) {
- super("filesystem", instance);
-
- if (!fs.existsSync(this.FS_ROOT)) {
- fs.mkdirSync(this.FS_ROOT, { recursive: true });
- }
-
- if (!fs.existsSync(this.SYSTEM_PATH)) {
- fs.mkdirSync(this.SYSTEM_PATH, {
- recursive: true,
- });
- }
-
- if (!fs.existsSync(this.CACHE_PATH)) {
- fs.mkdirSync(this.CACHE_PATH, { recursive: true });
- }
-
- if (!fs.existsSync(path.join(this.FS_ROOT, "assets/login"))) {
- fs.mkdirSync(path.join(this.FS_ROOT, "assets/login"), {
- recursive: true,
- });
- }
-
- if (!fs.existsSync(path.join(this.FS_ROOT, "assets/login/banner.png"))) {
- if (!fs.existsSync(path.join(this.AUTOINSTALL_PATH, "assets/login/banner.png"))) {
- fs.cpSync(path.join(this.SRC_ROOT, "assets/placeholder/banner.png"), path.join(this.FS_ROOT, "assets/login/banner.png"));
- } else {
- fs.cpSync(path.join(this.AUTOINSTALL_PATH, "assets/login/banner.png"), path.join(this.FS_ROOT, "assets/login/banner.png"));
- }
- }
-
- if (!fs.existsSync(path.join(this.FS_ROOT, "assets/login/background.png"))) {
- fs.cpSync(path.join(this.SRC_ROOT, "assets/wallpapers/pexels-steve-29708303.jpg"), path.join(this.FS_ROOT, "assets/login/background.png"));
- }
-
- if (!fs.existsSync(path.join(this.SYSTEM_PATH, "fs_template_files"))) {
- fs.mkdirSync(path.join(this.SYSTEM_PATH, "fs_template_files"));
- fs.cpSync(path.join(this.SRC_ROOT, "assets/fs_template_files/"), path.join(this.SYSTEM_PATH, "fs_template_files"), { recursive: true });
- }
-
- if (!fs.existsSync(path.join(this.SYSTEM_PATH, "vite"))) {
- fs.mkdirSync(path.join(this.SYSTEM_PATH, "vite"));
- }
-
- this._internalAssets = new Map();
- this._internalAssetPaths = new Map();
- }
-
- getApplicationSrc(applicationId: string) {
- console.log(this.instance.sys.applications.availableApplications.find((a) => a.manifest?.id === applicationId)?.path);
-
- return this.instance.sys.applications.availableApplications.find((a) => a.manifest?.id === applicationId)?.path;
- }
-
- // Create a directory if it does not already exist
- // @returns {true} if created
- // @returns {false} if already exists
- async createDirectoryIfNotExists(path: string): Promise {
- if (
- await fs.promises
- .access(path)
- .then(() => true)
- .catch(() => false)
- ) {
- return false;
- }
-
- await fs.promises.mkdir(path, { recursive: true });
-
- return true;
- }
-
- getFileType(path: string) {
- const extension = path.split(".").pop();
-
- switch (extension) {
- case "avif":
- case "webp":
- case "jpg":
- case "jpeg":
- case "png":
- case "gif":
- return "image";
- case "txt":
- case "json":
- case "js":
- case "ts":
- case "tsx":
- case "scss":
- case "sass":
- case "yml":
- case "yaml":
- case "xml":
- case "py":
- case "toml":
- case "rs":
- case "html":
- case "htm":
- case "css":
- case "jsm":
- case "tsm":
- case "tsc":
- case "jsc":
- case "sql":
- case "lock":
- return "plaintext";
- }
-
- this.log.warning(`Unknown file format: '${path}' ext: '${extension}'`);
-
- return undefined;
- }
-
- // returns an endpoint where the asset located at the provided path can be loaded from on the client
- // defaults to 3hrs validity
- serveFile(userId: number, fsPath: string, isPublic: boolean = false, dontCachePath = false, validMs: number = 21600000): string {
- if (!dontCachePath) {
- const existingAsset = this._internalAssetPaths.get(fsPath);
-
- if (existingAsset) {
- this._internalAssets.get(existingAsset)!.validUntil = Date.now() + validMs;
-
- return `/api/asset/raw/${existingAsset}`;
- }
- }
-
- const assetId = crypto.randomUUID();
-
- this._internalAssets.set(assetId, {
- path: fsPath,
- userId: userId,
- validUntil: Date.now() + validMs,
- public: isPublic,
- });
- this._internalAssetPaths.set(fsPath, assetId);
-
- this.log.info(`Serving asset at '${fsPath}' as '${assetId}'`);
-
- return `/api/asset/raw/${assetId}`;
- }
-
- // TODO: properly implement this instead of this hack.
- async getUserPermissions(userId: number, fsPath: string): Promise<{ read: boolean; write: boolean }> {
- const user = (await this.instance.sys.users.getUserById(userId))!;
-
- if (await user.isAdministrator())
- return {
- read: true,
- write: true,
- };
-
- if (fsPath.startsWith(path.join(this.FS_ROOT, `/users/${userId}`)))
- return {
- read: true,
- write: true,
- };
-
- const db = this.instance.sys.database.postgres();
-
- const userGroups = (await user.getGroups()) || [];
- const userIdArray = userId ? [userId.toString()] : [];
-
- const validUserGroups = userGroups.filter(Boolean);
-
- return {
- read: false,
- write: false,
- };
-
- // const permissions = await db`
- // SELECT
- // EXISTS(
- // SELECT 1
- // FROM public.filesystem_permission_overrides
- // WHERE file_path = ${fsPath}
- // AND read_permission_groups && ${validUserGroups}::TEXT[]
- // OR read_permission_users @> ARRAY[${userIdArray}::TEXT]
- // ) AS read_permission,
- // EXISTS(
- // SELECT 1
- // FROM public.filesystem_permission_overrides
- // WHERE file_path = ${fsPath}
- // AND write_permission_groups && ${validUserGroups}::TEXT[]
- // OR write_permission_users @> ARRAY[${userIdArray}::TEXT]
- // ) AS write_permission
- // `;
- // return {
- // read: permissions[0].read_permission,
- // write: permissions[0].write_permission,
- // };
- }
-
- getUserHomeDirectory(userId: number): string {
- return path.join(this.FS_ROOT, `/users/${userId}`);
- }
-
- override async startup(): Promise {
- await super.startup();
-
- this.instance.sys.event.on(WorkspacesEvent.BeforeStartupComplete, async () => {
- const db = this.instance.sys.database.postgres();
-
- await db`CREATE TABLE IF NOT EXISTS filesystem_permission_overrides (
- file_path TEXT,
- recursive BOOL,
- read_permission_groups TEXT[],
- read_permission_users TEXT[],
- write_permission_groups TEXT[],
- write_permission_users TEXT[],
- owner TEXT[]
-)`;
-
- if (!this.instance.sys.configuration.isDevMode) {
- if (fs.existsSync(this.CACHE_PATH)) {
- fs.rmSync(this.CACHE_PATH, { recursive: true });
- fs.mkdirSync(this.CACHE_PATH, { recursive: true });
- }
- } else {
- this.log.warning("Cache was not cleared as we are running in devMode");
- }
- });
-
- return true;
- }
-}
+import fs from "node:fs";
+import path from "node:path";
+import type { Instance } from "../index.ts";
+import System from "../system.ts";
+import { WorkspacesEvent } from "./events.ts";
+
+export default class FilesystemSystem extends System {
+ readonly SRC_ROOT = path.resolve("./backend/src/");
+ readonly FS_ROOT = path.resolve("./fs/");
+ readonly CACHE_PATH = path.join(this.FS_ROOT, "cache");
+ readonly SYSTEM_PATH = path.join(this.FS_ROOT, "system");
+ readonly AUTOINSTALL_PATH = path.join(process.cwd(), "autoinstall");
+
+ _internalAssets: Map;
+ _internalAssetPaths: Map;
+
+ constructor(instance: Instance) {
+ super("filesystem", instance);
+
+ if (!fs.existsSync(this.FS_ROOT)) {
+ fs.mkdirSync(this.FS_ROOT, { recursive: true });
+ }
+
+ if (!fs.existsSync(this.SYSTEM_PATH)) {
+ fs.mkdirSync(this.SYSTEM_PATH, {
+ recursive: true,
+ });
+ }
+
+ if (!fs.existsSync(this.CACHE_PATH)) {
+ fs.mkdirSync(this.CACHE_PATH, { recursive: true });
+ }
+
+ if (!fs.existsSync(path.join(this.FS_ROOT, "assets/login"))) {
+ fs.mkdirSync(path.join(this.FS_ROOT, "assets/login"), {
+ recursive: true,
+ });
+ }
+
+ if (!fs.existsSync(path.join(this.FS_ROOT, "assets/login/banner.png"))) {
+ if (!fs.existsSync(path.join(this.AUTOINSTALL_PATH, "assets/login/banner.png"))) {
+ fs.cpSync(path.join(this.SRC_ROOT, "assets/placeholder/banner.png"), path.join(this.FS_ROOT, "assets/login/banner.png"));
+ } else {
+ fs.cpSync(path.join(this.AUTOINSTALL_PATH, "assets/login/banner.png"), path.join(this.FS_ROOT, "assets/login/banner.png"));
+ }
+ }
+
+ if (!fs.existsSync(path.join(this.FS_ROOT, "assets/login/background.png"))) {
+ fs.cpSync(path.join(this.SRC_ROOT, "assets/wallpapers/pexels-steve-29708303.jpg"), path.join(this.FS_ROOT, "assets/login/background.png"));
+ }
+
+ if (!fs.existsSync(path.join(this.SYSTEM_PATH, "fs_template_files"))) {
+ fs.mkdirSync(path.join(this.SYSTEM_PATH, "fs_template_files"));
+ fs.cpSync(path.join(this.SRC_ROOT, "assets/fs_template_files/"), path.join(this.SYSTEM_PATH, "fs_template_files"), { recursive: true });
+ }
+
+ if (!fs.existsSync(path.join(this.SYSTEM_PATH, "vite"))) {
+ fs.mkdirSync(path.join(this.SYSTEM_PATH, "vite"));
+ }
+
+ this._internalAssets = new Map();
+ this._internalAssetPaths = new Map();
+ }
+
+ getApplicationSrc(applicationId: string) {
+ console.log(this.instance.sys.applications.availableApplications.find((a) => a.manifest?.id === applicationId)?.path);
+
+ return this.instance.sys.applications.availableApplications.find((a) => a.manifest?.id === applicationId)?.path;
+ }
+
+ // Create a directory if it does not already exist
+ // @returns {true} if created
+ // @returns {false} if already exists
+ async createDirectoryIfNotExists(path: string): Promise {
+ if (
+ await fs.promises
+ .access(path)
+ .then(() => true)
+ .catch(() => false)
+ ) {
+ return false;
+ }
+
+ await fs.promises.mkdir(path, { recursive: true });
+
+ return true;
+ }
+
+ getFileType(path: string) {
+ const extension = path.split(".").pop();
+
+ switch (extension) {
+ case "avif":
+ case "webp":
+ case "jpg":
+ case "jpeg":
+ case "png":
+ case "gif":
+ return "image";
+ case "txt":
+ case "json":
+ case "js":
+ case "ts":
+ case "tsx":
+ case "scss":
+ case "sass":
+ case "yml":
+ case "yaml":
+ case "xml":
+ case "py":
+ case "toml":
+ case "rs":
+ case "html":
+ case "htm":
+ case "css":
+ case "jsm":
+ case "tsm":
+ case "tsc":
+ case "jsc":
+ case "sql":
+ case "lock":
+ return "plaintext";
+ }
+
+ this.log.warning(`Unknown file format: '${path}' ext: '${extension}'`);
+
+ return undefined;
+ }
+
+ // returns an endpoint where the asset located at the provided path can be loaded from on the client
+ // defaults to 3hrs validity
+ serveFile(userId: number, fsPath: string, isPublic: boolean = false, dontCachePath = false, validMs: number = 21600000): string {
+ if (!dontCachePath) {
+ const existingAsset = this._internalAssetPaths.get(fsPath);
+
+ if (existingAsset) {
+ this._internalAssets.get(existingAsset)!.validUntil = Date.now() + validMs;
+
+ return `/api/asset/raw/${existingAsset}`;
+ }
+ }
+
+ const assetId = crypto.randomUUID();
+
+ this._internalAssets.set(assetId, {
+ path: fsPath,
+ userId: userId,
+ validUntil: Date.now() + validMs,
+ public: isPublic,
+ });
+ this._internalAssetPaths.set(fsPath, assetId);
+
+ this.log.info(`Serving asset at '${fsPath}' as '${assetId}'`);
+
+ return `/api/asset/raw/${assetId}`;
+ }
+
+ // TODO: properly implement this instead of this hack.
+ async getUserPermissions(userId: number, fsPath: string): Promise<{ read: boolean; write: boolean }> {
+ const user = (await this.instance.sys.users.getUserById(userId))!;
+
+ if (await user.isAdministrator())
+ return {
+ read: true,
+ write: true,
+ };
+
+ if (fsPath.startsWith(path.join(this.FS_ROOT, `/users/${userId}`)))
+ return {
+ read: true,
+ write: true,
+ };
+
+ const db = this.instance.sys.database.postgres();
+
+ const userGroups = (await user.getGroups()) || [];
+ const userIdArray = userId ? [userId.toString()] : [];
+
+ const validUserGroups = userGroups.filter(Boolean);
+
+ return {
+ read: false,
+ write: false,
+ };
+
+ // const permissions = await db`
+ // SELECT
+ // EXISTS(
+ // SELECT 1
+ // FROM public.filesystem_permission_overrides
+ // WHERE file_path = ${fsPath}
+ // AND read_permission_groups && ${validUserGroups}::TEXT[]
+ // OR read_permission_users @> ARRAY[${userIdArray}::TEXT]
+ // ) AS read_permission,
+ // EXISTS(
+ // SELECT 1
+ // FROM public.filesystem_permission_overrides
+ // WHERE file_path = ${fsPath}
+ // AND write_permission_groups && ${validUserGroups}::TEXT[]
+ // OR write_permission_users @> ARRAY[${userIdArray}::TEXT]
+ // ) AS write_permission
+ // `;
+ // return {
+ // read: permissions[0].read_permission,
+ // write: permissions[0].write_permission,
+ // };
+ }
+
+ getUserHomeDirectory(userId: number): string {
+ return path.join(this.FS_ROOT, `/users/${userId}`);
+ }
+
+ override async startup(): Promise {
+ await super.startup();
+
+ this.instance.sys.event.on(WorkspacesEvent.BeforeStartupComplete, async () => {
+ const db = this.instance.sys.database.postgres();
+
+ await db`CREATE TABLE IF NOT EXISTS filesystem_permission_overrides (
+ file_path TEXT,
+ recursive BOOL,
+ read_permission_groups TEXT[],
+ read_permission_users TEXT[],
+ write_permission_groups TEXT[],
+ write_permission_users TEXT[],
+ owner TEXT[]
+)`;
+
+ if (!this.instance.sys.configuration.isDevMode) {
+ if (fs.existsSync(this.CACHE_PATH)) {
+ fs.rmSync(this.CACHE_PATH, { recursive: true });
+ fs.mkdirSync(this.CACHE_PATH, { recursive: true });
+ }
+ } else {
+ this.log.warning("Cache was not cleared as we are running in devMode");
+ }
+ });
+
+ return true;
+ }
+}
diff --git a/instance/src/systems/image.ts b/backend/src/systems/image.ts
similarity index 100%
rename from instance/src/systems/image.ts
rename to backend/src/systems/image.ts
diff --git a/instance/src/systems/notifications.ts b/backend/src/systems/notifications.ts
similarity index 100%
rename from instance/src/systems/notifications.ts
rename to backend/src/systems/notifications.ts
diff --git a/instance/src/systems/reverseProxy.ts b/backend/src/systems/reverseProxy.ts
similarity index 100%
rename from instance/src/systems/reverseProxy.ts
rename to backend/src/systems/reverseProxy.ts
diff --git a/instance/src/systems/settings.ts b/backend/src/systems/settings.ts
similarity index 100%
rename from instance/src/systems/settings.ts
rename to backend/src/systems/settings.ts
diff --git a/instance/src/systems/settings/applicationSetting/applicationSetting.ts b/backend/src/systems/settings/applicationSetting/applicationSetting.ts
similarity index 100%
rename from instance/src/systems/settings/applicationSetting/applicationSetting.ts
rename to backend/src/systems/settings/applicationSetting/applicationSetting.ts
diff --git a/instance/src/systems/settings/applicationSetting/booleanSetting.ts b/backend/src/systems/settings/applicationSetting/booleanSetting.ts
similarity index 100%
rename from instance/src/systems/settings/applicationSetting/booleanSetting.ts
rename to backend/src/systems/settings/applicationSetting/booleanSetting.ts
diff --git a/instance/src/systems/settings/applicationSetting/numberSetting.ts b/backend/src/systems/settings/applicationSetting/numberSetting.ts
similarity index 100%
rename from instance/src/systems/settings/applicationSetting/numberSetting.ts
rename to backend/src/systems/settings/applicationSetting/numberSetting.ts
diff --git a/instance/src/systems/settings/applicationSetting/stringListSetting.ts b/backend/src/systems/settings/applicationSetting/stringListSetting.ts
similarity index 100%
rename from instance/src/systems/settings/applicationSetting/stringListSetting.ts
rename to backend/src/systems/settings/applicationSetting/stringListSetting.ts
diff --git a/instance/src/systems/settings/applicationSetting/stringSetting.ts b/backend/src/systems/settings/applicationSetting/stringSetting.ts
similarity index 100%
rename from instance/src/systems/settings/applicationSetting/stringSetting.ts
rename to backend/src/systems/settings/applicationSetting/stringSetting.ts
diff --git a/instance/src/systems/terminal.ts b/backend/src/systems/terminal.ts
similarity index 100%
rename from instance/src/systems/terminal.ts
rename to backend/src/systems/terminal.ts
diff --git a/instance/src/systems/trpc.ts b/backend/src/systems/trpc.ts
similarity index 76%
rename from instance/src/systems/trpc.ts
rename to backend/src/systems/trpc.ts
index 4d14b75..e780081 100644
--- a/instance/src/systems/trpc.ts
+++ b/backend/src/systems/trpc.ts
@@ -1,15 +1,13 @@
import type { TRPCBuiltRouter } from "@trpc/server";
import { type FetchCreateContextFnOptions, fetchRequestHandler } from "@trpc/server/adapters/fetch";
-import type { BunRequest, Server } from "bun";
import type { Instance } from "../index.ts";
import System from "../system.ts";
-import type { createTRPCContext } from "./trpcRouter.ts";
export default class TRPCSystem extends System {
registeredRouters: {
basePath: string;
router: TRPCBuiltRouter;
- createContext: (opts: FetchCreateContextFnOptions, server: Server>) => object;
+ createContext: (opts: FetchCreateContextFnOptions, server: Deno.HttpServer) => object;
}[];
constructor(instance: Instance) {
@@ -23,7 +21,7 @@ export default class TRPCSystem extends System {
return true;
}
- private attemptTRPCRequest(req: BunRequest, server: Server>) {
+ private attemptTRPCRequest(req: Request, server: Deno.HttpServer) {
const url = new URL(req.url);
for (const router of this.registeredRouters) {
@@ -47,10 +45,10 @@ export default class TRPCSystem extends System {
serve(options: {
routes: {
[path: string]: {
- GET?: (req: BunRequest) => Promise;
- POST?: (req: BunRequest) => Promise;
- DELETE?: (req: BunRequest) => Promise;
- PUT?: (req: BunRequest) => Promise;
+ GET?: (req: Request) => Promise;
+ POST?: (req: Request) => Promise;
+ DELETE?: (req: Request) => Promise;
+ PUT?: (req: Request) => Promise;
};
};
fetch(request: any, server: any): Response;
@@ -62,7 +60,7 @@ export default class TRPCSystem extends System {
...options,
port: 3563,
hostname: "0.0.0.0",
- async fetch(req: BunRequest, server: Server>) {
+ async fetch(req: Request, server: Deno.HttpServer) {
try {
const trpcResponse = await self.attemptTRPCRequest(req, server);
if (trpcResponse) {
diff --git a/instance/src/systems/trpcRouter.ts b/backend/src/systems/trpcRouter.ts
similarity index 91%
rename from instance/src/systems/trpcRouter.ts
rename to backend/src/systems/trpcRouter.ts
index 80b4db3..e7620d1 100644
--- a/instance/src/systems/trpcRouter.ts
+++ b/backend/src/systems/trpcRouter.ts
@@ -1,745 +1,731 @@
-import * as nodeCrypto from "node:crypto";
-import { on } from "node:events";
-import { initTRPC, TRPCError } from "@trpc/server";
-import type { FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
-import type { Server } from "bun";
-import * as hiBase32 from "hi-base32";
-import * as OTPAuth from "otpauth";
-import z from "zod";
-import type { Instance } from "../index.ts";
-import { AuthorizedDeviceType } from "./authorization.ts";
-import { WorkspacesFeatureFlags } from "./configuration.ts";
-import { type WorkspacesNotification, WorkspacesNotificationEventEmitterEvent } from "./notifications.ts";
-import type { WorkspacesUser } from "./users.ts";
-
-export const createTRPCContext = (instance: Instance) => (opt: FetchCreateContextFnOptions, server: Server