From 94af2a20653d0813dce9a3aa6aa9eec8ceabd8e3 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 28 Mar 2026 11:31:16 +0200 Subject: [PATCH] a new 'base app' which gon get deployed on prem --- README.md | 5 +- apps/front/old.server.ts | 150 +++ apps/front/view.html | 4 +- apps/main/src/hooks.server.ts | 3 +- .../lib/domains/device/device.vm.svelte.ts | 6 +- .../src/lib/domains/link/link.vm.svelte.ts | 196 ++++ .../src/routes/(main)/dashboard/+page.svelte | 258 +---- .../src/routes/(main)/devices/+page.svelte | 400 +++++++- .../routes/(main)/devices/[id]/+page.svelte | 1 + .../main/src/routes/(main)/links/+page.svelte | 419 +++++++- apps/main/src/routes/layout.css | 91 +- apps/ws-scrcpy | 1 + dokploy-install.sh | 229 +++++ .../migrations/0000_colorful_the_leader.sql | 142 +++ .../db/migrations/meta/0000_snapshot.json | 920 ++++++++++++++++++ packages/db/migrations/meta/_journal.json | 13 + packages/settings/index.ts | 9 - 17 files changed, 2525 insertions(+), 322 deletions(-) create mode 100644 apps/front/old.server.ts create mode 100644 apps/main/src/lib/domains/link/link.vm.svelte.ts create mode 100644 apps/main/src/routes/(main)/devices/[id]/+page.svelte create mode 160000 apps/ws-scrcpy create mode 100644 dokploy-install.sh create mode 100644 packages/db/migrations/0000_colorful_the_leader.sql create mode 100644 packages/db/migrations/meta/0000_snapshot.json create mode 100644 packages/db/migrations/meta/_journal.json diff --git a/README.md b/README.md index 2036b74..8c068af 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Currently in alpha. Greenfield. Subject to change. 1. Admin generates a unique link and assigns it to a user (or a slot). 2. User opens that link in their browser — served by `apps/front`. -3. User waits while a Docker-Android device is allocated to their session. +3. User is shown a loading screen for good UX purposes 4. User is prompted to install the PWA. 5. User opens the PWA — they are routed into a live stream of their assigned Android instance. 6. Admin manages the entire fleet from `apps/main` (the dashboard), which communicates with `apps/orchestrator` running on each VPS to control Docker-Android containers. @@ -54,11 +54,12 @@ Currently in alpha. Greenfield. Subject to change. - [ ] Admin dashboard: Links page — generate links, view detail, configure linked device, revoke, delete - [ ] `apps/front`: validate incoming link token on request - [ ] `apps/front`: return appropriate error page for invalid/expired/revoked links +- [ ] Front: keep on checking for link status change, if it gets revoked, we cutoff the connection ### PWA & User Session Flow (`apps/front`) - [ ] `apps/front`: serve static PWA shell (HTML + manifest + service worker) -- [ ] `apps/front`: wait/loading page — poll for device allocation status +- [ ] `apps/front`: wait/loading page — just for show with a 3-5s duration - [ ] `apps/front`: PWA install prompt flow (beforeinstallprompt handling) - [ ] `apps/front`: session binding — tie the PWA launch to the user's allocated device - [ ] `apps/front`: route/proxy authenticated PWA requests to the Android instance stream diff --git a/apps/front/old.server.ts b/apps/front/old.server.ts new file mode 100644 index 0000000..e6de9a7 --- /dev/null +++ b/apps/front/old.server.ts @@ -0,0 +1,150 @@ +/** + * Mobile Proxy Test — Bun Reverse Proxy Server + * + * Sits on port 3000 (what ngrok tunnels) and: + * GET / -> serves our clean mobile viewer (view.html) + * Everything else -> reverse-proxied to ws-scrcpy on port 8000 + * + * WebSocket upgrades are proxied transparently so the ws-scrcpy + * player running inside our iframe can talk to the scrcpy server. + */ + +const WS_SCRCPY_ORIGIN = "http://localhost:8000"; +const PROXY_PORT = 3000; + +const viewHtml = await Bun.file( + new URL("./view.html", import.meta.url).pathname, +).text(); + +const server = Bun.serve({ + port: PROXY_PORT, + hostname: "0.0.0.0", + + async fetch(req, server) { + const url = new URL(req.url); + + // Serve our clean mobile viewer at root only + // (the iframe loads ws-scrcpy's own page via /?scrcpy=1) + if ( + (url.pathname === "/" || url.pathname === "/index.html") && + !url.searchParams.has("scrcpy") + ) { + return new Response(viewHtml, { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); + } + + // Upgrade WebSocket connections — proxy to ws-scrcpy + if (req.headers.get("upgrade")?.toLowerCase() === "websocket") { + const targetUrl = `${WS_SCRCPY_ORIGIN.replace("http", "ws")}${url.pathname}${url.search}`; + + // Use Bun's WebSocket upgrade to establish a client-side WS to ws-scrcpy + // and bridge the two connections + const success = server.upgrade(req, { + data: { targetUrl }, + }); + + if (success) return undefined; + return new Response("WebSocket upgrade failed", { status: 400 }); + } + + // Proxy all other HTTP requests to ws-scrcpy + const targetUrl = `${WS_SCRCPY_ORIGIN}${url.pathname}${url.search}`; + + try { + const proxyRes = await fetch(targetUrl, { + method: req.method, + headers: req.headers, + body: + req.method !== "GET" && req.method !== "HEAD" + ? req.body + : undefined, + }); + + // Clone response with CORS headers stripped / adjusted + const headers = new Headers(proxyRes.headers); + headers.delete("content-encoding"); // Bun handles this + + return new Response(proxyRes.body, { + status: proxyRes.status, + statusText: proxyRes.statusText, + headers, + }); + } catch (err) { + console.error(`Proxy error for ${url.pathname}:`, err); + return new Response( + "ws-scrcpy not reachable — is it running on port 8000?", + { + status: 502, + }, + ); + } + }, + + websocket: { + async open(ws) { + const { targetUrl } = ws.data as { targetUrl: string }; + + // Open a WebSocket connection to ws-scrcpy + const upstream = new WebSocket(targetUrl); + + // Store upstream reference on ws data for cleanup + (ws.data as any).upstream = upstream; + + upstream.binaryType = "arraybuffer"; + + upstream.onopen = () => { + // Connection established, nothing extra needed + }; + + upstream.onmessage = (event) => { + try { + if (event.data instanceof ArrayBuffer) { + ws.sendBinary(new Uint8Array(event.data)); + } else { + ws.sendText(event.data); + } + } catch { + // Client disconnected + } + }; + + upstream.onclose = () => { + ws.close(); + }; + + upstream.onerror = (err) => { + console.error("Upstream WS error:", err); + ws.close(); + }; + }, + + message(ws, message) { + const upstream = (ws.data as any).upstream as WebSocket | undefined; + if (!upstream || upstream.readyState !== WebSocket.OPEN) return; + + try { + if (typeof message === "string") { + upstream.send(message); + } else { + upstream.send(message); + } + } catch { + // Upstream disconnected + } + }, + + close(ws) { + const upstream = (ws.data as any).upstream as WebSocket | undefined; + if (upstream && upstream.readyState === WebSocket.OPEN) { + upstream.close(); + } + }, + }, +}); + +console.log(`\n Mobile Proxy running on http://localhost:${server.port}`); +console.log(` Proxying to ws-scrcpy at ${WS_SCRCPY_ORIGIN}`); +console.log( + `\n Point ngrok at port ${server.port}, then open the ngrok URL on the user's phone.\n`, +); diff --git a/apps/front/view.html b/apps/front/view.html index 4f4d891..03e7f6c 100644 --- a/apps/front/view.html +++ b/apps/front/view.html @@ -163,9 +163,9 @@ `; function injectCSS(doc) { - if (doc.querySelector("#maitm-hide-css")) return; + if (doc.querySelector("#iotam-hide-css")) return; const style = doc.createElement("style"); - style.id = "maitm-hide-css"; + style.id = "iotam-hide-css"; style.textContent = HIDE_CSS; doc.head.appendChild(style); } diff --git a/apps/main/src/hooks.server.ts b/apps/main/src/hooks.server.ts index 45d5ffb..b1efe7c 100644 --- a/apps/main/src/hooks.server.ts +++ b/apps/main/src/hooks.server.ts @@ -22,8 +22,7 @@ export const first: Handle = async ({ event, resolve }) => { event.url.pathname.includes("/api/auth") || event.url.pathname.includes("/api/debug") || event.url.pathname.includes("/api/chat") || - event.url.pathname.includes("/auth") || - event.url.pathname.includes("/link") + event.url.pathname.includes("/auth") ) { return await resolve(event); } diff --git a/apps/main/src/lib/domains/device/device.vm.svelte.ts b/apps/main/src/lib/domains/device/device.vm.svelte.ts index 77ac221..fb67702 100644 --- a/apps/main/src/lib/domains/device/device.vm.svelte.ts +++ b/apps/main/src/lib/domains/device/device.vm.svelte.ts @@ -78,7 +78,9 @@ class DeviceViewModel { } toast.success("Device created"); this.showCreateDialog = false; - await this.fetchDevices(); + if (result.data) { + this.devices = [...this.devices, result.data as Device]; + } return true; } catch (error) { toast.error("Failed to create device", { @@ -108,7 +110,7 @@ class DeviceViewModel { return; } toast.success("Device deleted"); - await this.fetchDevices(); + this.devices = this.devices.filter((d) => d.id !== id); } catch (error) { toast.error("Failed to delete device", { description: diff --git a/apps/main/src/lib/domains/link/link.vm.svelte.ts b/apps/main/src/lib/domains/link/link.vm.svelte.ts new file mode 100644 index 0000000..e1c8c87 --- /dev/null +++ b/apps/main/src/lib/domains/link/link.vm.svelte.ts @@ -0,0 +1,196 @@ +import { + listLinksSQ, + createLinkSC, + revokeLinkSC, + deleteLinkSC, + assignDeviceSC, +} from "./link.remote"; +import { listDevicesSQ } from "../device/device.remote"; +import { toast } from "svelte-sonner"; + +type Link = { + id: number; + token: string; + status: string; + linkedDeviceId: number | null; + expiresAt: Date | null; + lastAccessedAt: Date | null; + createdAt: Date; + updatedAt: Date; +}; + +type DeviceOption = { + id: number; + title: string; + host: string; + status: string; +}; + +class LinkViewModel { + links = $state([]); + availableDevices = $state([]); + loading = $state(false); + creating = $state(false); + deletingId = $state(null); + revokingId = $state(null); + showCreateDialog = $state(false); + + async fetchLinks() { + this.loading = true; + try { + const result = await listLinksSQ(); + if (result?.error || !result?.data) { + toast.error( + result?.error?.message || "Failed to fetch links", + { + description: + result?.error?.description || "Please try again", + }, + ); + return; + } + this.links = result.data as Link[]; + } catch (error) { + toast.error("Failed to fetch links", { + description: + error instanceof Error + ? error.message + : "Please try again", + }); + } finally { + this.loading = false; + } + } + + async fetchDevicesForSelect() { + try { + const result = await listDevicesSQ(); + if (result?.data) { + this.availableDevices = (result.data as any[]).map((d) => ({ + id: d.id, + title: d.title, + host: d.host, + status: d.status, + })); + } + } catch { + // Non-critical — select will just be empty + } + } + + async createLink(data: { + linkedDeviceId?: number | null; + expiresAt?: Date | null; + }): Promise { + this.creating = true; + try { + const result = await createLinkSC(data); + if (result?.error) { + toast.error(result.error.message || "Failed to create link", { + description: + result.error.description || "Please try again", + }); + return false; + } + toast.success("Link created"); + this.showCreateDialog = false; + if (result.data) { + this.links = [...this.links, result.data as Link]; + } + return true; + } catch (error) { + toast.error("Failed to create link", { + description: + error instanceof Error + ? error.message + : "Please try again", + }); + return false; + } finally { + this.creating = false; + } + } + + async revokeLink(id: number) { + this.revokingId = id; + try { + const result = await revokeLinkSC({ id }); + if (result?.error) { + toast.error(result.error.message || "Failed to revoke link", { + description: + result.error.description || "Please try again", + }); + return; + } + toast.success("Link revoked"); + await this.fetchLinks(); + } catch (error) { + toast.error("Failed to revoke link", { + description: + error instanceof Error + ? error.message + : "Please try again", + }); + } finally { + this.revokingId = null; + } + } + + async deleteLink(id: number) { + this.deletingId = id; + try { + const result = await deleteLinkSC({ id }); + if (result?.error) { + toast.error(result.error.message || "Failed to delete link", { + description: + result.error.description || "Please try again", + }); + return; + } + toast.success("Link deleted"); + this.links = this.links.filter((l) => l.id !== id); + } catch (error) { + toast.error("Failed to delete link", { + description: + error instanceof Error + ? error.message + : "Please try again", + }); + } finally { + this.deletingId = null; + } + } + + async assignDevice(linkId: number, deviceId: number | null) { + try { + const result = await assignDeviceSC({ id: linkId, deviceId }); + if (result?.error) { + toast.error( + result.error.message || "Failed to assign device", + { + description: + result.error.description || "Please try again", + }, + ); + return; + } + toast.success(deviceId ? "Device assigned" : "Device unassigned"); + await this.fetchLinks(); + } catch (error) { + toast.error("Failed to assign device", { + description: + error instanceof Error + ? error.message + : "Please try again", + }); + } + } + + getDeviceName(deviceId: number | null): string { + if (!deviceId) return "—"; + const device = this.availableDevices.find((d) => d.id === deviceId); + return device ? device.title : `Device #${deviceId}`; + } +} + +export const linkVM = new LinkViewModel(); diff --git a/apps/main/src/routes/(main)/dashboard/+page.svelte b/apps/main/src/routes/(main)/dashboard/+page.svelte index 37d5813..0ad2a2d 100644 --- a/apps/main/src/routes/(main)/dashboard/+page.svelte +++ b/apps/main/src/routes/(main)/dashboard/+page.svelte @@ -1,267 +1,11 @@ - - -
-
- - Devices - - {mobileVM.devicesTotal} total - -
-
- - -
-
- -
- - { - mobileVM.devicesPage = 1; - void mobileVM.refreshDevices(); - }} - /> -
-
- - - {#if !mobileVM.devicesLoading && mobileVM.devices.length === 0} -
- No devices registered yet. -
- {:else} -
- {#each mobileVM.devices as device (device.id)} -
goto(`/devices/${device.id}`)} - onkeydown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - void goto(`/devices/${device.id}`); - } - }} - > -
-
-

{device.name}

-

- {device.externalDeviceId} -

-
- - e.stopPropagation()} - > - - - - - - Delete device? - - - This deletes the device and all related SMS/media data. - Files in storage linked to this device are also removed. - - - - Cancel - { - e.stopPropagation(); - await mobileVM.deleteDevice(device.id); - }} - > - Delete - - - - -
- -
-
-

Manufacturer / Model

-

{device.manufacturer} / {device.model}

-
-
-

Android

-

{device.androidVersion}

-
-
-

Created

-

- {new Date(device.createdAt).toLocaleString()} -

-
-
-

Last Ping

-

- {mobileVM.formatLastPing(device.lastPingAt)} -

-
-
-
- {/each} -
- - - {/if} -
-
+ dunno
diff --git a/apps/main/src/routes/(main)/devices/+page.svelte b/apps/main/src/routes/(main)/devices/+page.svelte index f074940..8f55d2b 100644 --- a/apps/main/src/routes/(main)/devices/+page.svelte +++ b/apps/main/src/routes/(main)/devices/+page.svelte @@ -1 +1,399 @@ -Show the running devices list here + + + + + +
+
+ + Devices + + {deviceVM.devices.length} total + +
+
+ + +
+
+
+ + + {#if !deviceVM.loading && deviceVM.devices.length === 0} +
+ No devices registered yet. +
+ {:else} + +
+ {#each deviceVM.devices as device (device.id)} +
+
+
+

+ {device.title} +

+

+ {device.host} +

+
+ + + {device.status} + +
+
+
+

Version

+

{device.version}

+
+
+

Active

+

{device.isActive ? "Yes" : "No"}

+
+
+

+ Container +

+

+ {device.containerId || "—"} +

+
+
+

+ WS Port +

+

{device.wsPort || "—"}

+
+
+
+ + + + + + + + Delete device? + + + This will permanently remove + "{device.title}" and unlink it + from any associated links. + + + + + Cancel + + + deviceVM.deleteDevice( + device.id, + )} + > + Delete + + + + +
+
+ {/each} +
+ + + + {/if} +
+
+
+ + + + + + Add Device + + Register a new Docker-Android instance. + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ + + + +
+
+
diff --git a/apps/main/src/routes/(main)/devices/[id]/+page.svelte b/apps/main/src/routes/(main)/devices/[id]/+page.svelte new file mode 100644 index 0000000..5714613 --- /dev/null +++ b/apps/main/src/routes/(main)/devices/[id]/+page.svelte @@ -0,0 +1 @@ +device id page diff --git a/apps/main/src/routes/(main)/links/+page.svelte b/apps/main/src/routes/(main)/links/+page.svelte index b3fe352..5bc8e9d 100644 --- a/apps/main/src/routes/(main)/links/+page.svelte +++ b/apps/main/src/routes/(main)/links/+page.svelte @@ -1 +1,418 @@ -everything related to links here + + + + + +
+
+ + Links + + {linkVM.links.length} total + +
+
+ + +
+
+
+ + + {#if !linkVM.loading && linkVM.links.length === 0} +
+ No links generated yet. +
+ {:else} + +
+ {#each linkVM.links as link (link.id)} +
+
+
+
+ + {link.token} + + +
+
+ + {link.status} + +
+
+
+

Device

+

+ {linkVM.getDeviceName( + link.linkedDeviceId, + )} +

+
+
+

Expires

+

{formatDate(link.expiresAt)}

+
+
+

+ Last Accessed +

+

{formatDate(link.lastAccessedAt)}

+
+
+

Created

+

{formatDate(link.createdAt)}

+
+
+
+ {#if link.status === "active"} + + {/if} + + + + + + + + Delete link? + + + This will permanently remove + this link. Anyone with this + token will no longer be able to + access the service. + + + + + Cancel + + + linkVM.deleteLink(link.id)} + > + Delete + + + + +
+
+ {/each} +
+ + + + {/if} +
+
+
+ + + + + + Generate Link + + Create a new access link. The token is auto-generated. + + +
+
+ + +

+ Optional. You can assign a device later. +

+
+
+ + +

+ Optional. Leave empty for no expiry. +

+
+ + + + +
+
+
diff --git a/apps/main/src/routes/layout.css b/apps/main/src/routes/layout.css index 5860334..e245be5 100644 --- a/apps/main/src/routes/layout.css +++ b/apps/main/src/routes/layout.css @@ -20,41 +20,41 @@ --popover: oklch(0.991 0 0); --popover-foreground: oklch(0 0 0); - /* --- main theme: lavender/royal purple --- */ - --primary: oklch(0.6 0.2 280); /* medium lavender purple */ + /* --- main theme: teal --- */ + --primary: oklch(0.6 0.15 180); /* medium teal */ --primary-foreground: oklch(0.99 0 0); - --secondary: oklch(0.93 0.05 285); /* soft pale lavender */ - --secondary-foreground: oklch(0.25 0.03 285); + --secondary: oklch(0.93 0.04 178); /* soft pale teal */ + --secondary-foreground: oklch(0.25 0.03 180); - --muted: oklch(0.96 0.01 275); - --muted-foreground: oklch(0.4 0.01 278); + --muted: oklch(0.96 0.01 175); + --muted-foreground: oklch(0.4 0.01 178); - --accent: oklch(0.86 0.08 275); /* lavender accent */ - --accent-foreground: oklch(0.5 0.15 280); + --accent: oklch(0.86 0.07 175); /* teal accent */ + --accent-foreground: oklch(0.5 0.12 180); --destructive: oklch(0.63 0.18 25); --destructive-foreground: oklch(1 0 0); - --border: oklch(0.92 0.02 284); + --border: oklch(0.92 0.02 178); --input: oklch(0.94 0 0); - --ring: oklch(0.6 0.2 280); + --ring: oklch(0.6 0.15 180); - /* charts — more variety but still within lavender spectrum */ - --chart-1: oklch(0.7 0.16 275); - --chart-2: oklch(0.6 0.2 280); - --chart-3: oklch(0.72 0.18 295); /* slightly more magenta */ - --chart-4: oklch(0.65 0.15 265); /* slightly bluer lavender */ - --chart-5: oklch(0.76 0.1 285); + /* charts — variety within teal spectrum */ + --chart-1: oklch(0.7 0.13 175); + --chart-2: oklch(0.6 0.15 180); + --chart-3: oklch(0.72 0.14 165); /* slightly more green-teal */ + --chart-4: oklch(0.65 0.12 190); /* slightly bluer teal */ + --chart-5: oklch(0.76 0.09 182); - --sidebar: oklch(0.97 0.01 280); + --sidebar: oklch(0.97 0.01 178); --sidebar-foreground: oklch(0 0 0); - --sidebar-primary: oklch(0.6 0.2 280); + --sidebar-primary: oklch(0.6 0.15 180); --sidebar-primary-foreground: oklch(1 0 0); - --sidebar-accent: oklch(0.92 0.02 284); - --sidebar-accent-foreground: oklch(0.2 0.02 280); - --sidebar-border: oklch(0.92 0.02 284); - --sidebar-ring: oklch(0.6 0.2 280); + --sidebar-accent: oklch(0.92 0.02 178); + --sidebar-accent-foreground: oklch(0.2 0.02 180); + --sidebar-border: oklch(0.92 0.02 178); + --sidebar-ring: oklch(0.6 0.15 180); --font-sans: Plus Jakarta Sans, sans-serif; --font-serif: Lora, serif; @@ -86,49 +86,48 @@ } .dark { - --background: oklch(0.23 0.01 278); + --background: oklch(0.23 0.01 178); --foreground: oklch(0.95 0 0); - --card: oklch(0.25 0.015 278); + --card: oklch(0.25 0.015 178); --card-foreground: oklch(0.95 0 0); - --popover: oklch(0.25 0.015 278); + --popover: oklch(0.25 0.015 178); --popover-foreground: oklch(0.95 0 0); - --primary: oklch(0.56 0.17 280); + --primary: oklch(0.56 0.13 180); --primary-foreground: oklch(0.97 0 0); - --secondary: oklch(0.35 0.03 280); + --secondary: oklch(0.35 0.03 180); --secondary-foreground: oklch(0.92 0 0); - --muted: oklch(0.33 0.02 280); - --muted-foreground: oklch(0.7 0.01 280); + --muted: oklch(0.33 0.02 178); + --muted-foreground: oklch(0.7 0.01 178); - --accent: oklch(0.44 0.1 278); - --accent-foreground: oklch(0.88 0.09 280); + --accent: oklch(0.44 0.08 178); + --accent-foreground: oklch(0.88 0.08 180); --destructive: oklch(0.7 0.17 25); --destructive-foreground: oklch(1 0 0); - --border: oklch(0.34 0.02 278); - --input: oklch(0.34 0.02 278); - --ring: oklch(0.65 0.22 280); - --ring: oklch(0.56 0.17 280); + --border: oklch(0.34 0.02 178); + --input: oklch(0.34 0.02 178); + --ring: oklch(0.56 0.13 180); - --chart-1: oklch(0.68 0.15 275); - --chart-2: oklch(0.62 0.2 280); - --chart-3: oklch(0.7 0.14 292); - --chart-4: oklch(0.65 0.16 265); - --chart-5: oklch(0.72 0.1 285); + --chart-1: oklch(0.68 0.12 175); + --chart-2: oklch(0.62 0.15 180); + --chart-3: oklch(0.7 0.11 165); + --chart-4: oklch(0.65 0.13 190); + --chart-5: oklch(0.72 0.09 182); - --sidebar: oklch(0.2 0.01 278); + --sidebar: oklch(0.2 0.01 178); --sidebar-foreground: oklch(0.95 0 0); - --sidebar-primary: oklch(0.56 0.17 280); + --sidebar-primary: oklch(0.56 0.13 180); --sidebar-primary-foreground: oklch(0.97 0 0); - --sidebar-accent: oklch(0.35 0.03 280); - --sidebar-accent-foreground: oklch(0.65 0.22 280); - --sidebar-border: oklch(0.34 0.02 278); - --sidebar-ring: oklch(0.65 0.22 280); + --sidebar-accent: oklch(0.35 0.03 180); + --sidebar-accent-foreground: oklch(0.65 0.15 180); + --sidebar-border: oklch(0.34 0.02 178); + --sidebar-ring: oklch(0.65 0.15 180); } @theme inline { diff --git a/apps/ws-scrcpy b/apps/ws-scrcpy new file mode 160000 index 0000000..ef273d9 --- /dev/null +++ b/apps/ws-scrcpy @@ -0,0 +1 @@ +Subproject commit ef273d97c6ac0c05d03b41d4b55d59a25b95c505 diff --git a/dokploy-install.sh b/dokploy-install.sh new file mode 100644 index 0000000..aa20f90 --- /dev/null +++ b/dokploy-install.sh @@ -0,0 +1,229 @@ +#!/bin/bash +install_dokploy() { + if [ "$(id -u)" != "0" ]; then + echo "This script must be run as root" >&2 + exit 1 + fi + + # check if is Mac OS + if [ "$(uname)" = "Darwin" ]; then + echo "This script must be run on Linux" >&2 + exit 1 + fi + + # check if is running inside a container + if [ -f /.dockerenv ]; then + echo "This script must be run on Linux" >&2 + exit 1 + fi + + # check if something is running on port 80 + if ss -tulnp | grep ':80 ' >/dev/null; then + echo "Error: something is already running on port 80" >&2 + exit 1 + fi + + # check if something is running on port 443 + if ss -tulnp | grep ':443 ' >/dev/null; then + echo "Error: something is already running on port 443" >&2 + exit 1 + fi + + # check if something is running on port 3000 + if ss -tulnp | grep ':3000 ' >/dev/null; then + echo "Error: something is already running on port 3000" >&2 + echo "Dokploy requires port 3000 to be available. Please stop any service using this port." >&2 + exit 1 + fi + + command_exists() { + command -v "$@" > /dev/null 2>&1 + } + + if command_exists docker; then + echo "Docker already installed" + else + curl -sSL https://get.docker.com | sh + fi + + docker swarm leave --force 2>/dev/null + + get_ip() { + local ip="" + + # Try IPv4 first + # First attempt: ifconfig.io + ip=$(curl -4s --connect-timeout 5 https://ifconfig.io 2>/dev/null) + + # Second attempt: icanhazip.com + if [ -z "$ip" ]; then + ip=$(curl -4s --connect-timeout 5 https://icanhazip.com 2>/dev/null) + fi + + # Third attempt: ipecho.net + if [ -z "$ip" ]; then + ip=$(curl -4s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) + fi + + # If no IPv4, try IPv6 + if [ -z "$ip" ]; then + # Try IPv6 with ifconfig.io + ip=$(curl -6s --connect-timeout 5 https://ifconfig.io 2>/dev/null) + + # Try IPv6 with icanhazip.com + if [ -z "$ip" ]; then + ip=$(curl -6s --connect-timeout 5 https://icanhazip.com 2>/dev/null) + fi + + # Try IPv6 with ipecho.net + if [ -z "$ip" ]; then + ip=$(curl -6s --connect-timeout 5 https://ipecho.net/plain 2>/dev/null) + fi + fi + + if [ -z "$ip" ]; then + echo "Error: Could not determine server IP address automatically (neither IPv4 nor IPv6)." >&2 + echo "Please set the ADVERTISE_ADDR environment variable manually." >&2 + echo "Example: export ADVERTISE_ADDR=" >&2 + exit 1 + fi + + echo "$ip" + } + + advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}" + echo "Using advertise address: $advertise_addr" + + docker swarm init \ + --advertise-addr $advertise_addr \ + --default-addr-pool 10.200.0.0/16 \ + --default-addr-pool-mask-length 24 + + if [ $? -ne 0 ]; then + echo "Error: Failed to initialize Docker Swarm" >&2 + exit 1 + fi + + echo "Swarm initialized" + + docker network rm -f dokploy-network 2>/dev/null + docker network create --driver overlay --attachable --subnet 10.201.0.0/16 dokploy-network + + echo "Network created" + + mkdir -p /etc/dokploy + + chmod 777 /etc/dokploy + + # Generate secure random password for Postgres + POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-32) + + # Store password as Docker Secret (encrypted and secure) + echo "$POSTGRES_PASSWORD" | docker secret create dokploy_postgres_password - 2>/dev/null || true + + echo "Generated secure database credentials (stored in Docker Secrets)" + + docker service create \ + --name dokploy-postgres \ + --constraint 'node.role==manager' \ + --network dokploy-network \ + --env POSTGRES_USER=dokploy \ + --env POSTGRES_DB=dokploy \ + --secret source=dokploy_postgres_password,target=/run/secrets/postgres_password \ + --env POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password \ + --mount type=volume,source=dokploy-postgres,target=/var/lib/postgresql/data \ + postgres:16 + + docker service create \ + --name dokploy-redis \ + --constraint 'node.role==manager' \ + --network dokploy-network \ + --mount type=volume,source=dokploy-redis,target=/data \ + redis:7 + + # Installation + docker service create \ + --name dokploy \ + --replicas 1 \ + --network dokploy-network \ + --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \ + --mount type=bind,source=/etc/dokploy,target=/etc/dokploy \ + --mount type=volume,source=dokploy,target=/root/.docker \ + --secret source=dokploy_postgres_password,target=/run/secrets/postgres_password \ + --publish published=3000,target=3000,mode=host \ + --update-parallelism 1 \ + --update-order stop-first \ + --constraint 'node.role == manager' \ + -e ADVERTISE_ADDR=$advertise_addr \ + -e POSTGRES_PASSWORD_FILE=/run/secrets/postgres_password \ + dokploy/dokploy:latest + + + docker run -d \ + --name dokploy-traefik \ + --restart always \ + -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \ + -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -p 80:80/tcp \ + -p 443:443/tcp \ + -p 443:443/udp \ + traefik:v3.6.7 + + docker network connect dokploy-network dokploy-traefik + + + # Optional: Use docker service create instead of docker run + # docker service create \ + # --name dokploy-traefik \ + # --constraint 'node.role==manager' \ + # --network dokploy-network \ + # --mount type=bind,source=/etc/dokploy/traefik/traefik.yml,target=/etc/traefik/traefik.yml \ + # --mount type=bind,source=/etc/dokploy/traefik/dynamic,target=/etc/dokploy/traefik/dynamic \ + # --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock,readonly \ + # --publish mode=host,published=443,target=443 \ + # --publish mode=host,published=80,target=80 \ + # --publish mode=host,published=443,target=443,protocol=udp \ + # traefik:v3.6.7 + + GREEN="\033[0;32m" + YELLOW="\033[1;33m" + BLUE="\033[0;34m" + NC="\033[0m" # No Color + + format_ip_for_url() { + local ip="$1" + if echo "$ip" | grep -q ':'; then + # IPv6 + echo "[${ip}]" + else + # IPv4 + echo "${ip}" + fi + } + + formatted_addr=$(format_ip_for_url "$advertise_addr") + echo "" + printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n" + printf "${BLUE}Wait 15 seconds for the server to start${NC}\n" + printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n" +} + +update_dokploy() { + echo "Updating Dokploy..." + + # Pull the latest image + docker pull dokploy/dokploy:latest + + # Update the service + docker service update --image dokploy/dokploy:latest dokploy + + echo "Dokploy has been updated to the latest version." +} + +# Main script execution +if [ "$1" = "update" ]; then + update_dokploy +else + install_dokploy +fi diff --git a/packages/db/migrations/0000_colorful_the_leader.sql b/packages/db/migrations/0000_colorful_the_leader.sql new file mode 100644 index 0000000..d68b2d0 --- /dev/null +++ b/packages/db/migrations/0000_colorful_the_leader.sql @@ -0,0 +1,142 @@ +CREATE TABLE "two_factor" ( + "id" text PRIMARY KEY NOT NULL, + "secret" text NOT NULL, + "backup_codes" json, + "user_id" text NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "twofa_sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "session_id" text NOT NULL, + "verification_token" text NOT NULL, + "code_used" text, + "status" varchar(16) NOT NULL, + "attempts" integer DEFAULT 0 NOT NULL, + "max_attempts" integer DEFAULT 5 NOT NULL, + "verified_at" timestamp, + "expires_at" timestamp NOT NULL, + "created_at" timestamp NOT NULL, + "ip_address" text DEFAULT '', + "user_agent" text DEFAULT '', + CONSTRAINT "twofa_sessions_verification_token_unique" UNIQUE("verification_token") +); +--> statement-breakpoint +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "image" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "username" text, + "display_username" text, + "role" text, + "banned" boolean DEFAULT false, + "ban_reason" text, + "ban_expires" timestamp, + "onboarding_done" boolean DEFAULT false, + "last2_fa_verified_at" timestamp, + "parent_id" text, + CONSTRAINT "user_email_unique" UNIQUE("email"), + CONSTRAINT "user_username_unique" UNIQUE("username") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "device" ( + "id" serial PRIMARY KEY NOT NULL, + "title" text NOT NULL, + "version" text NOT NULL, + "status" varchar(16) DEFAULT 'offline' NOT NULL, + "is_active" boolean DEFAULT false NOT NULL, + "container_id" text, + "host" text NOT NULL, + "ws_port" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notifications" ( + "id" serial PRIMARY KEY NOT NULL, + "title" text NOT NULL, + "body" text NOT NULL, + "priority" varchar(12) DEFAULT 'normal' NOT NULL, + "type" varchar(12) NOT NULL, + "category" varchar(64), + "is_read" boolean DEFAULT false NOT NULL, + "is_archived" boolean DEFAULT false NOT NULL, + "action_url" text, + "action_type" varchar(16), + "action_data" json, + "icon" varchar(64), + "user_id" text NOT NULL, + "sent_at" timestamp NOT NULL, + "read_at" timestamp, + "expires_at" timestamp, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "link" ( + "id" serial PRIMARY KEY NOT NULL, + "token" text NOT NULL, + "status" varchar(16) DEFAULT 'active' NOT NULL, + "linked_device_id" integer, + "expires_at" timestamp, + "last_accessed_at" timestamp, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + CONSTRAINT "link_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "task" ( + "id" text PRIMARY KEY NOT NULL, + "type" varchar(32) NOT NULL, + "status" varchar(16) NOT NULL, + "progress" integer DEFAULT 0 NOT NULL, + "payload" json, + "result" json, + "error" json, + "user_id" text NOT NULL, + "resource_id" text NOT NULL, + "started_at" timestamp, + "completed_at" timestamp, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "twofa_sessions" ADD CONSTRAINT "twofa_sessions_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "link" ADD CONSTRAINT "link_linked_device_id_device_id_fk" FOREIGN KEY ("linked_device_id") REFERENCES "public"."device"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "task" ADD CONSTRAINT "task_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0000_snapshot.json b/packages/db/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..12ae902 --- /dev/null +++ b/packages/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,920 @@ +{ + "id": "c0dc4466-4211-49aa-97b0-917cc0c30871", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.twofa_sessions": { + "name": "twofa_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verification_token": { + "name": "verification_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code_used": { + "name": "code_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "twofa_sessions_user_id_user_id_fk": { + "name": "twofa_sessions_user_id_user_id_fk", + "tableFrom": "twofa_sessions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "twofa_sessions_verification_token_unique": { + "name": "twofa_sessions_verification_token_unique", + "nullsNotDistinct": false, + "columns": [ + "verification_token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "onboarding_done": { + "name": "onboarding_done", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "last2_fa_verified_at": { + "name": "last2_fa_verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_username_unique": { + "name": "user_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device": { + "name": "device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'offline'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "container_id": { + "name": "container_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ws_port": { + "name": "ws_port", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "type": { + "name": "type", + "type": "varchar(12)", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "is_read": { + "name": "is_read", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "action_url": { + "name": "action_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_type": { + "name": "action_type", + "type": "varchar(16)", + "primaryKey": false, + "notNull": false + }, + "action_data": { + "name": "action_data", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "notifications_user_id_user_id_fk": { + "name": "notifications_user_id_user_id_fk", + "tableFrom": "notifications", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.link": { + "name": "link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "linked_device_id": { + "name": "linked_device_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "link_linked_device_id_device_id_fk": { + "name": "link_linked_device_id_device_id_fk", + "tableFrom": "link", + "tableTo": "device", + "columnsFrom": [ + "linked_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "link_token_unique": { + "name": "link_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task": { + "name": "task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(16)", + "primaryKey": false, + "notNull": true + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "task_user_id_user_id_fk": { + "name": "task_user_id_user_id_fk", + "tableFrom": "task", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json new file mode 100644 index 0000000..0777fae --- /dev/null +++ b/packages/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1774650657798, + "tag": "0000_colorful_the_leader", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/settings/index.ts b/packages/settings/index.ts index 62515b7..916479f 100644 --- a/packages/settings/index.ts +++ b/packages/settings/index.ts @@ -19,10 +19,6 @@ export const settingsSchema = v.object({ debugKey: v.string(), processorApiUrl: v.string(), - appBuilderApiUrl: v.string(), - appBuilderAssetsPublicUrl: v.string(), - clientDownloadedApkName: v.string(), - mobileAppApiUrl: v.string(), betterAuthUrl: v.string(), betterAuthSecret: v.string(), @@ -108,11 +104,6 @@ function loadSettings(): Settings { "APP_BUILDER_ASSETS_PUBLIC_URL", "http://localhost:3001", ), - clientDownloadedApkName: getEnv( - "CLIENT_DOWNLOADED_APK_NAME", - "illusory-client.apk", - ), - mobileAppApiUrl: getEnv("MOBILE_APP_API_URL"), betterAuthUrl: getEnv("BETTER_AUTH_URL"), betterAuthSecret: getEnv("BETTER_AUTH_SECRET"),