From 73d9865c82f9b3a41d01632bfe3e0ff960d8e515 Mon Sep 17 00:00:00 2001 From: incognitotgt Date: Wed, 8 May 2024 14:45:59 -0400 Subject: [PATCH] FEAT: v0.5-beta Admin dash, custom server, and other things: - start admin dash - use a custom nextjs server (no more next-ws) - webmanifest - use db transactions - bundle analyzer - use swr for session fetching - other unknown things that i forgot about. --- .vscode/css_custom_data.json | 13 - .vscode/extensions.json | 3 - next.config.mjs | 13 +- package.json | 20 +- pnpm-lock.yaml | 732 +++++++++++++++++++- server.ts | 75 ++ src/app/(main)/admin/images/columns.tsx | 30 + src/app/(main)/admin/images/page.tsx | 19 + src/app/(main)/admin/layout.tsx | 18 +- src/app/(main)/admin/page.tsx | 46 +- src/app/(main)/admin/sessions/columns.tsx | 146 ++++ src/app/(main)/admin/sessions/page.tsx | 19 + src/app/(main)/admin/users/columns.tsx | 21 + src/app/(main)/admin/users/page.tsx | 19 + src/app/(main)/layout.tsx | 144 +--- src/app/(main)/page.tsx | 51 +- src/app/(main)/session-date.tsx | 15 + src/app/api/session/[slug]/files/route.ts | 4 +- src/app/api/session/[slug]/route.ts | 31 + src/app/api/vnc/[slug]/route.ts | 66 -- src/app/icon.svg | 2 +- src/app/manifest.ts | 20 + src/app/view/[slug]/page.tsx | 222 +++--- src/components/create-session-button.tsx | 10 +- src/components/data-table/column-header.tsx | 55 ++ src/components/data-table/column-toggle.tsx | 50 ++ src/components/data-table/pagination.tsx | 82 +++ src/components/navbar.tsx | 151 +++- src/components/sidebar.tsx | 55 ++ src/components/ui/checkbox.tsx | 28 + src/components/ui/data-table.tsx | 77 ++ src/components/ui/table.tsx | 72 ++ src/hooks/use-hydration.ts | 8 + src/lib/docker.ts | 42 +- src/lib/drizzle/db.ts | 16 +- src/lib/drizzle/relational-types.ts | 40 ++ src/lib/drizzle/schema.ts | 1 + src/lib/drizzle/seed.ts | 2 +- src/lib/session/get-session.ts | 24 +- src/lib/session/index.ts | 77 +- src/lib/session/purge.ts | 21 +- src/lib/session/session-running.ts | 13 +- src/middleware.ts | 2 +- 43 files changed, 2061 insertions(+), 494 deletions(-) delete mode 100644 .vscode/css_custom_data.json delete mode 100644 .vscode/extensions.json create mode 100644 server.ts create mode 100644 src/app/(main)/admin/images/columns.tsx create mode 100644 src/app/(main)/admin/images/page.tsx create mode 100644 src/app/(main)/admin/sessions/columns.tsx create mode 100644 src/app/(main)/admin/sessions/page.tsx create mode 100644 src/app/(main)/admin/users/columns.tsx create mode 100644 src/app/(main)/admin/users/page.tsx create mode 100644 src/app/(main)/session-date.tsx create mode 100644 src/app/api/session/[slug]/route.ts delete mode 100644 src/app/api/vnc/[slug]/route.ts create mode 100644 src/app/manifest.ts create mode 100644 src/components/data-table/column-header.tsx create mode 100644 src/components/data-table/column-toggle.tsx create mode 100644 src/components/data-table/pagination.tsx create mode 100644 src/components/sidebar.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/data-table.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/hooks/use-hydration.ts create mode 100644 src/lib/drizzle/relational-types.ts diff --git a/.vscode/css_custom_data.json b/.vscode/css_custom_data.json deleted file mode 100644 index 1f546dc..0000000 --- a/.vscode/css_custom_data.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 1.1, - "atDirectives": [ - { - "name": "@tailwind", - "description": "Use the @tailwind directive to insert Tailwind's `base`, `components`, `utilities`, and `screens` styles into your CSS." - }, - { - "name": "@apply", - "description": "Use the @apply directive to apply existing classes to an element." - } - ] -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index c68b6c5..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"] -} diff --git a/next.config.mjs b/next.config.mjs index c2d6616..86211b5 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,6 +1,7 @@ // @ts-check +import NextBundleAnalyzer from "@next/bundle-analyzer"; import { execSync } from "node:child_process"; -await import("next-ws/server/index.js").then((m) => m.verifyPatch()); +const withBundleAnalyzer = NextBundleAnalyzer(); /** @type {import('next').NextConfig} */ const nextConfig = { images: { @@ -14,12 +15,13 @@ const nextConfig = { ], }, env: { - GIT_COMMIT: execSync("git rev-parse HEAD").toString().trim(), + GIT_COMMIT: process.env.NODE_ENV === "production" ? execSync("git rev-parse HEAD").toString().trim() : "DEVELOP", BUILD_DATE: Date.now().toString(), }, experimental: { typedRoutes: true, instrumentationHook: true, + webpackBuildWorker: true, serverActions: { allowedOrigins: ["localhost:3000", "*.use.devtunnels.ms"], }, @@ -41,5 +43,8 @@ const nextConfig = { return config; }, }; - -export default nextConfig; +let config = nextConfig; +if (process.env.ANALYZE) { + config = withBundleAnalyzer(nextConfig); +} +export default config; diff --git a/package.json b/package.json index af95899..a78964d 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,25 @@ { "name": "stardust", - "version": "0.5.0-alpha", + "version": "0.5.0-beta", "private": true, "type": "module", "scripts": { - "dev": "next dev", + "dev": "tsx server.ts", "build": "next build", - "start": "next start", + "start": "NODE_ENV=production tsx server.ts", "lint": "biome lint --apply .", "format": "biome format --write .", "drizzle-kit": "drizzle-kit", - "db:push": "drizzle-kit push:pg", - "postinstall": "pnpm dlx next-ws-cli@latest patch" + "db:push": "drizzle-kit push:pg" }, "dependencies": { + "@next/bundle-analyzer": "^14.2.3", "@novnc/novnc": "^1.4.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -32,9 +33,12 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", + "@tanstack/react-table": "^8.16.0", "@types/dockerode": "^3.3.26", + "@types/ws": "^8.5.10", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", + "consola": "^3.2.3", "dockerode": "^4.0.2", "dotenv": "^16.4.5", "drizzle-orm": "^0.30.4", @@ -42,7 +46,6 @@ "next": "14.2.3", "next-auth": "4.24.7", "next-themes": "^0.3.0", - "next-ws": "^1.0.1", "node-loader": "^2.0.0", "pg": "^8.11.3", "pg-native": "^3.0.1", @@ -50,11 +53,13 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "server-only": "^0.0.1", + "sharp": "^0.33.3", "sonner": "^1.4.41", "swr": "^2.2.5", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", - "ws": "^8.16.0" + "tsx": "^4.9.1", + "ws": "^8.17.0" }, "devDependencies": { "@biomejs/biome": "1.7.0", @@ -63,7 +68,6 @@ "@types/pg": "^8.11.4", "@types/react": "^18.2.67", "@types/react-dom": "^18.2.22", - "@types/ws": "^8.5.10", "autoprefixer": "^10.4.19", "drizzle-kit": "^0.20.17", "postcss": "^8.4.38", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d715568..5e83372 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@next/bundle-analyzer': + specifier: ^14.2.3 + version: 14.2.3 '@novnc/novnc': specifier: ^1.4.0 version: 1.4.0 @@ -20,6 +23,9 @@ dependencies: '@radix-ui/react-avatar': specifier: ^1.0.4 version: 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-checkbox': + specifier: ^1.0.4 + version: 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-context-menu': specifier: ^2.1.5 version: 2.1.5(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) @@ -59,15 +65,24 @@ dependencies: '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-table': + specifier: ^8.16.0 + version: 8.16.0(react-dom@18.2.0)(react@18.2.0) '@types/dockerode': specifier: ^3.3.26 version: 3.3.26 + '@types/ws': + specifier: ^8.5.10 + version: 8.5.10 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 clsx: specifier: ^2.1.0 version: 2.1.0 + consola: + specifier: ^3.2.3 + version: 3.2.3 dockerode: specifier: ^4.0.2 version: 4.0.2 @@ -89,9 +104,6 @@ dependencies: next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.2.0)(react@18.2.0) - next-ws: - specifier: ^1.0.1 - version: 1.0.1(next@14.2.3)(react@18.2.0)(ws@8.16.0) node-loader: specifier: ^2.0.0 version: 2.0.0(webpack@5.91.0) @@ -113,6 +125,9 @@ dependencies: server-only: specifier: ^0.0.1 version: 0.0.1 + sharp: + specifier: ^0.33.3 + version: 0.33.3 sonner: specifier: ^1.4.41 version: 1.4.41(react-dom@18.2.0)(react@18.2.0) @@ -125,9 +140,12 @@ dependencies: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.1) + tsx: + specifier: ^4.9.1 + version: 4.9.1 ws: - specifier: ^8.16.0 - version: 8.16.0 + specifier: ^8.17.0 + version: 8.17.0 devDependencies: '@biomejs/biome': @@ -148,9 +166,6 @@ devDependencies: '@types/react-dom': specifier: ^18.2.22 version: 18.2.22 - '@types/ws': - specifier: ^8.5.10 - version: 8.5.10 autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.38) @@ -275,6 +290,19 @@ packages: dev: true optional: true + /@discoveryjs/json-ext@0.5.7: + resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} + engines: {node: '>=10.0.0'} + dev: false + + /@emnapi/runtime@1.1.1: + resolution: {integrity: sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==} + requiresBuild: true + dependencies: + tslib: 2.6.2 + dev: false + optional: true + /@esbuild-kit/core-utils@3.3.2: resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} dependencies: @@ -297,6 +325,15 @@ packages: requiresBuild: true optional: true + /@esbuild/aix-ppc64@0.20.2: + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm64@0.18.20: resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} @@ -314,6 +351,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-arm64@0.20.2: + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-arm@0.18.20: resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} @@ -331,6 +377,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-arm@0.20.2: + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/android-x64@0.18.20: resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} @@ -348,6 +403,15 @@ packages: requiresBuild: true optional: true + /@esbuild/android-x64@0.20.2: + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-arm64@0.18.20: resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} @@ -365,6 +429,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-arm64@0.20.2: + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/darwin-x64@0.18.20: resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} @@ -382,6 +455,15 @@ packages: requiresBuild: true optional: true + /@esbuild/darwin-x64@0.20.2: + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-arm64@0.18.20: resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} @@ -399,6 +481,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-arm64@0.20.2: + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/freebsd-x64@0.18.20: resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} @@ -416,6 +507,15 @@ packages: requiresBuild: true optional: true + /@esbuild/freebsd-x64@0.20.2: + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm64@0.18.20: resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} @@ -433,6 +533,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm64@0.20.2: + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-arm@0.18.20: resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} @@ -450,6 +559,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-arm@0.20.2: + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ia32@0.18.20: resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} @@ -467,6 +585,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ia32@0.20.2: + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-loong64@0.18.20: resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} engines: {node: '>=12'} @@ -484,6 +611,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-loong64@0.20.2: + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-mips64el@0.18.20: resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} @@ -501,6 +637,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-mips64el@0.20.2: + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-ppc64@0.18.20: resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} @@ -518,6 +663,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-ppc64@0.20.2: + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-riscv64@0.18.20: resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} @@ -535,6 +689,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-riscv64@0.20.2: + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-s390x@0.18.20: resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} @@ -552,6 +715,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-s390x@0.20.2: + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/linux-x64@0.18.20: resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} @@ -569,6 +741,15 @@ packages: requiresBuild: true optional: true + /@esbuild/linux-x64@0.20.2: + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@esbuild/netbsd-x64@0.18.20: resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} @@ -586,6 +767,15 @@ packages: requiresBuild: true optional: true + /@esbuild/netbsd-x64@0.20.2: + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/openbsd-x64@0.18.20: resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} @@ -603,6 +793,15 @@ packages: requiresBuild: true optional: true + /@esbuild/openbsd-x64@0.20.2: + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: false + optional: true + /@esbuild/sunos-x64@0.18.20: resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} @@ -620,6 +819,15 @@ packages: requiresBuild: true optional: true + /@esbuild/sunos-x64@0.20.2: + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-arm64@0.18.20: resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} @@ -637,6 +845,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-arm64@0.20.2: + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-ia32@0.18.20: resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} @@ -654,6 +871,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-ia32@0.20.2: + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@esbuild/win32-x64@0.18.20: resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} @@ -671,6 +897,15 @@ packages: requiresBuild: true optional: true + /@esbuild/win32-x64@0.20.2: + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@floating-ui/core@1.6.0: resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} dependencies: @@ -714,6 +949,194 @@ packages: zod: 3.22.4 dev: true + /@img/sharp-darwin-arm64@0.33.3: + resolution: {integrity: sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.2 + dev: false + optional: true + + /@img/sharp-darwin-x64@0.33.3: + resolution: {integrity: sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.2 + dev: false + optional: true + + /@img/sharp-libvips-darwin-arm64@1.0.2: + resolution: {integrity: sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==} + engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-darwin-x64@1.0.2: + resolution: {integrity: sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==} + engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-arm64@1.0.2: + resolution: {integrity: sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-arm@1.0.2: + resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-s390x@1.0.2: + resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-x64@1.0.2: + resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linuxmusl-arm64@1.0.2: + resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linuxmusl-x64@1.0.2: + resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-linux-arm64@0.33.3: + resolution: {integrity: sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.2 + dev: false + optional: true + + /@img/sharp-linux-arm@0.33.3: + resolution: {integrity: sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==} + engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.2 + dev: false + optional: true + + /@img/sharp-linux-s390x@0.33.3: + resolution: {integrity: sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==} + engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.2 + dev: false + optional: true + + /@img/sharp-linux-x64@0.33.3: + resolution: {integrity: sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.2 + dev: false + optional: true + + /@img/sharp-linuxmusl-arm64@0.33.3: + resolution: {integrity: sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + dev: false + optional: true + + /@img/sharp-linuxmusl-x64@0.33.3: + resolution: {integrity: sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + requiresBuild: true + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + dev: false + optional: true + + /@img/sharp-wasm32@0.33.3: + resolution: {integrity: sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [wasm32] + requiresBuild: true + dependencies: + '@emnapi/runtime': 1.1.1 + dev: false + optional: true + + /@img/sharp-win32-ia32@0.33.3: + resolution: {integrity: sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-win32-x64@0.33.3: + resolution: {integrity: sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -757,6 +1180,15 @@ packages: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + /@next/bundle-analyzer@14.2.3: + resolution: {integrity: sha512-Z88hbbngMs7njZKI8kTJIlpdLKYfMSLwnsqYe54AP4aLmgL70/Ynx/J201DQ+q2Lr6FxFw1uCeLGImDrHOl2ZA==} + dependencies: + webpack-bundle-analyzer: 4.10.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /@next/env@14.2.3: resolution: {integrity: sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==} dev: false @@ -874,6 +1306,10 @@ packages: requiresBuild: true optional: true + /@polka/url@1.0.0-next.25: + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + dev: false + /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: @@ -1007,6 +1443,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.67)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.67)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.67)(react@18.2.0) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.67)(react@18.2.0) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.67)(react@18.2.0) + '@types/react': 18.2.67 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} peerDependencies: @@ -1851,6 +2315,23 @@ packages: tslib: 2.6.2 dev: false + /@tanstack/react-table@8.16.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-rKRjnt8ostqN2fercRVOIH/dq7MAmOENCMvVlKx6P9Iokhh6woBGnIZEkqsY/vEJf1jN3TqLOb34xQGLVRuhAg==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@tanstack/table-core': 8.16.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@tanstack/table-core@8.16.0: + resolution: {integrity: sha512-dCG8vQGk4js5v88/k83tTedWOwjGnIyONrKpHpfmSJB8jwFHl8GSu1sBBxbtACVAPtAQgwNxl0rw1d3RqRM1Tg==} + engines: {node: '>=12'} + dev: false + /@types/docker-modem@3.0.6: resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} dependencies: @@ -1942,8 +2423,8 @@ packages: /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: - '@types/node': 20.11.30 - dev: true + '@types/node': 20.12.7 + dev: false /@webassemblyjs/ast@1.12.1: resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} @@ -2067,6 +2548,11 @@ packages: acorn: 8.11.3 dev: false + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: false + /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} @@ -2318,6 +2804,24 @@ packages: /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + requiresBuild: true + + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + requiresBuild: true + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: false + + /color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + requiresBuild: true + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + dev: false /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -2327,11 +2831,21 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: false + /commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} dev: true + /consola@3.2.3: + resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} + engines: {node: ^14.18.0 || >=16.10.0} + dev: false + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -2382,6 +2896,10 @@ packages: type: 2.7.2 dev: true + /debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + dev: false + /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2393,6 +2911,12 @@ packages: dependencies: ms: 2.1.2 + /detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + /detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false @@ -2553,6 +3077,10 @@ packages: react: 18.2.0 dev: false + /duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -2700,10 +3228,46 @@ packages: '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 + /esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + dev: false + /escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: false + /eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -2828,7 +3392,6 @@ packages: resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} dependencies: resolve-pkg-maps: 1.0.0 - dev: true /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2872,6 +3435,13 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: false + /gzip-size@6.0.0: + resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} + engines: {node: '>=10'} + dependencies: + duplexer: 0.1.2 + dev: false + /hanji@0.0.5: resolution: {integrity: sha512-Abxw1Lq+TnYiL4BueXqMau222fPSPMFtya8HdpWsz/xVAhifXou71mPh/kY2+08RgFcVccjG3uZHs6K5HAe3zw==} dependencies: @@ -2899,6 +3469,10 @@ packages: engines: {node: '>=16.0.0'} dev: true + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: false + /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} dev: false @@ -2919,6 +3493,11 @@ packages: loose-envify: 1.4.0 dev: false + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + requiresBuild: true + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -2948,6 +3527,11 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: false + /is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} dev: true @@ -3152,6 +3736,11 @@ packages: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: false + /mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + dev: false + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -3215,18 +3804,6 @@ packages: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} dev: true - /next-ws@1.0.1(next@14.2.3)(react@18.2.0)(ws@8.16.0): - resolution: {integrity: sha512-OIxASK7gU17y4J84FRnLA0kSAj5sDpC2DPgtiNQCevNbf3GXk7AOuqY+6kwcw2iVG5aDOoxT8TLsNRtHb+/Znw==} - peerDependencies: - next: '>=13.1.1' - react: '*' - ws: '*' - dependencies: - next: 14.2.3(react-dom@18.2.0)(react@18.2.0) - react: 18.2.0 - ws: 8.16.0 - dev: false - /next@14.2.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==} engines: {node: '>=18.17.0'} @@ -3321,6 +3898,11 @@ packages: dependencies: wrappy: 1.0.2 + /opener@1.5.2: + resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} + hasBin: true + dev: false + /openid-client@5.6.5: resolution: {integrity: sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==} dependencies: @@ -3727,7 +4309,6 @@ packages: /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - dev: true /resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} @@ -3775,7 +4356,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -3787,6 +4367,36 @@ packages: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} dev: false + /sharp@0.33.3: + resolution: {integrity: sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==} + engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0} + requiresBuild: true + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.0 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.3 + '@img/sharp-darwin-x64': 0.33.3 + '@img/sharp-libvips-darwin-arm64': 1.0.2 + '@img/sharp-libvips-darwin-x64': 1.0.2 + '@img/sharp-libvips-linux-arm': 1.0.2 + '@img/sharp-libvips-linux-arm64': 1.0.2 + '@img/sharp-libvips-linux-s390x': 1.0.2 + '@img/sharp-libvips-linux-x64': 1.0.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + '@img/sharp-linux-arm': 0.33.3 + '@img/sharp-linux-arm64': 0.33.3 + '@img/sharp-linux-s390x': 0.33.3 + '@img/sharp-linux-x64': 0.33.3 + '@img/sharp-linuxmusl-arm64': 0.33.3 + '@img/sharp-linuxmusl-x64': 0.33.3 + '@img/sharp-wasm32': 0.33.3 + '@img/sharp-win32-ia32': 0.33.3 + '@img/sharp-win32-x64': 0.33.3 + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3801,6 +4411,22 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + requiresBuild: true + dependencies: + is-arrayish: 0.3.2 + dev: false + + /sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + dev: false + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} dev: true @@ -4088,6 +4714,11 @@ packages: dependencies: is-number: 7.0.0 + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: false + /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -4095,6 +4726,17 @@ packages: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: false + /tsx@4.9.1: + resolution: {integrity: sha512-CqSJaYyZ6GEqnGtPuMPQHvUwRGU6VHSVF+RDxoOmRg/XD4aF0pD973tKhoUYGQtdcoCHcSOGk34ioFaP+vYcMQ==} + engines: {node: '>=18.0.0'} + hasBin: true + dependencies: + esbuild: 0.20.2 + get-tsconfig: 4.7.3 + optionalDependencies: + fsevents: 2.3.3 + dev: false + /tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} dev: false @@ -4183,6 +4825,29 @@ packages: graceful-fs: 4.2.11 dev: false + /webpack-bundle-analyzer@4.10.1: + resolution: {integrity: sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==} + engines: {node: '>= 10.13.0'} + hasBin: true + dependencies: + '@discoveryjs/json-ext': 0.5.7 + acorn: 8.11.3 + acorn-walk: 8.3.2 + commander: 7.2.0 + debounce: 1.2.1 + escape-string-regexp: 4.0.0 + gzip-size: 6.0.0 + html-escaper: 2.0.2 + is-plain-object: 5.0.0 + opener: 1.5.2 + picocolors: 1.0.0 + sirv: 2.0.4 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + /webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} @@ -4258,8 +4923,21 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - /ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + /ws@7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /ws@8.17.0: + resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..d791635 --- /dev/null +++ b/server.ts @@ -0,0 +1,75 @@ +import "dotenv/config"; + +import { createServer } from "node:http"; +import { Socket } from "node:net"; +import { getSession as getContainerSession } from "@/lib/session/get-session"; +import { consola } from "consola"; +import next from "next"; +import { getSession } from "next-auth/react"; +import { WebSocketServer } from "ws"; +const port = Number.parseInt(process.env.PORT as string) || 3000; +const dev = process.env.NODE_ENV !== "production"; +if (process.argv.includes("--turbo")) { + process.env.TURBOPACK = "1"; +} +const server = createServer(); +const app = next({ dev, port, httpServer: server }); +consola.start(`✨ Stardust: Starting ${dev ? "development" : "production"} server...`); +await app.prepare(); +const nextRequest = app.getRequestHandler(); +const nextUpgrade = app.getUpgradeHandler(); +const websockify = new WebSocketServer({ noServer: true }); +websockify.on("connection", async (ws, req) => { + const userSession = await getSession({ req }); + if (!userSession) { + ws.close(1008, "Unauthorized"); + return; + } + const id = req.url?.split("/")[2]; + if (!id) { + ws.close(1008, "Missing ID"); + return; + } + const { vncPort } = (await getContainerSession(id, userSession)) || {}; + if (!vncPort) { + ws.close(1008, "Session not found"); + return; + } + const socket = new Socket(); + socket.connect(vncPort, process.env.CONTAINER_HOST as string); + ws.on("message", (message: Uint8Array) => { + socket.write(message); + }); + ws.on("close", (code, reason) => { + consola.info( + `✨ Stardust: Connection closed with code ${code} and ${reason ? `reason ${reason.toString()}` : "no reason"}`, + ); + socket.end(); + }); + + socket.on("data", (data) => { + ws.send(data); + }); + + socket.on("error", (err) => { + consola.warn(`✨ Stardust: ${err.message}`); + ws.close(); + }); + + socket.on("close", () => { + ws.close(); + }); +}); +server.on("request", nextRequest); +server.on("upgrade", async (req, socket, head) => { + if (req.url?.includes("websockify")) { + websockify.handleUpgrade(req, socket, head, (ws) => { + websockify.emit("connection", ws, req, websockify); + }); + } else { + nextUpgrade(req, socket, head); + } +}); +server.listen(port, () => { + consola.success(`✨ Stardust: Server listening on ${port}`); +}); diff --git a/src/app/(main)/admin/images/columns.tsx b/src/app/(main)/admin/images/columns.tsx new file mode 100644 index 0000000..6748eba --- /dev/null +++ b/src/app/(main)/admin/images/columns.tsx @@ -0,0 +1,30 @@ +"use client"; +import type { SelectImageRelation } from "@/lib/drizzle/relational-types"; +import type { ColumnDef } from "@tanstack/react-table"; +import Image from "next/image"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "friendlyName", + header: "Name", + }, + { + accessorKey: "dockerImage", + header: "Docker Image", + }, + { + accessorKey: "category", + header: "Category", + }, + { + accessorKey: "icon", + header: "Icon", + cell: ({ row }) => ( + {row.original.friendlyName} + ), + }, + { + accessorKey: "pulled", + header: "Pulled", + }, +]; diff --git a/src/app/(main)/admin/images/page.tsx b/src/app/(main)/admin/images/page.tsx new file mode 100644 index 0000000..b6d9ae2 --- /dev/null +++ b/src/app/(main)/admin/images/page.tsx @@ -0,0 +1,19 @@ +import { db } from "@/lib/drizzle/db"; +import { columns } from "./columns"; +import { DataTable } from "@/components/ui/data-table"; + +export default async function AdminPage() { + const sessions = await db.query.image.findMany({ + with: { + session: true, + }, + }); + return ( +
+

Images

+
+ +
+
+ ); +} diff --git a/src/app/(main)/admin/layout.tsx b/src/app/(main)/admin/layout.tsx index c4115ff..cdbbb98 100644 --- a/src/app/(main)/admin/layout.tsx +++ b/src/app/(main)/admin/layout.tsx @@ -2,17 +2,21 @@ import { getAuthSession } from "@/lib/auth"; import { db, user } from "@/lib/drizzle/db"; import { eq } from "drizzle-orm"; import { redirect } from "next/navigation"; +import { AdminSidebar } from "@/components/sidebar"; export default async function AdminLayout({ children }: { children: React.ReactNode }) { const userSession = await getAuthSession(); - const { isAdmin } = ( - await db - .select({ isAdmin: user.isAdmin }) - .from(user) - .where(eq(user.email, userSession?.user?.email as string)) - )[0]; + const [{ isAdmin }] = await db + .select({ isAdmin: user.isAdmin }) + .from(user) + .where(eq(user.email, userSession?.user?.email as string)); if (!isAdmin) { return redirect("/"); } - return children; + return ( + <> + +
{children}
+ + ); } diff --git a/src/app/(main)/admin/page.tsx b/src/app/(main)/admin/page.tsx index 3858ebd..45bf69e 100644 --- a/src/app/(main)/admin/page.tsx +++ b/src/app/(main)/admin/page.tsx @@ -1,3 +1,45 @@ -export default function AdminPage() { - return
coming soon (hopefully)
; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { getAuthSession } from "@/lib/auth"; +import { db, image, session, user } from "@/lib/drizzle/db"; +import { Container, Users } from "lucide-react"; + +export default async function AdminPage() { + const userSession = await getAuthSession(); + const { users, sessions } = await db.transaction(async (tx) => { + const users = await tx.select().from(user); + const sessions = await tx.query.session.findMany({ + with: { user: true }, + }); + return { users, sessions }; + }); + const activeUsers = sessions.map((s) => s.user); + return ( +
+

Welcome, {userSession.user?.name}

+
+ + + Sessions + + + +
{sessions.length}
+

+ {activeUsers.length} user{activeUsers.length === 1 ? "" : "s"} active +

+
+
+ + + Users + + + +
{users.length}
+

{users.filter((u) => u.isAdmin).length} admins

+
+
+
+
+ ); } diff --git a/src/app/(main)/admin/sessions/columns.tsx b/src/app/(main)/admin/sessions/columns.tsx new file mode 100644 index 0000000..49bf26d --- /dev/null +++ b/src/app/(main)/admin/sessions/columns.tsx @@ -0,0 +1,146 @@ +"use client"; +import { DataTableColumnHeader } from "@/components/data-table/column-header"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import type { SelectSessionRelation } from "@/lib/drizzle/relational-types"; +import { deleteSession, manageSession } from "@/lib/session"; +import type { ColumnDef } from "@tanstack/react-table"; +import { MoreHorizontal } from "lucide-react"; +import { toast } from "sonner"; +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "id", + header: "Container ID", + cell: ({ row }) => { + return row.original.id.slice(0, 7); + }, + }, + { + accessorKey: "user", + header: ({ column }) => , + cell: ({ row }) => { + return row.original.user.email; + }, + }, + { accessorKey: "dockerImage", header: ({ column }) => }, + { + accessorKey: "createdAt", + header: "Created at", + cell: ({ row }) => { + const date = new Date(row.original.createdAt); + return date.toLocaleString(); + }, + }, + { + accessorKey: "expiresAt", + header: "Expires at", + cell: ({ row }) => { + const date = new Date(row.original.expiresAt); + return date.toLocaleString(); + }, + }, + { + id: "actions", + header: ({ table }) => { + return table.getFilteredSelectedRowModel().rows.length > 0 ? ( + + + + + + Actions + + toast.promise( + () => + Promise.all( + table.getFilteredSelectedRowModel().rows.map((s) => deleteSession(s.original.id, true)), + ), + { + loading: "Deleting containers...", + success: "Sessions deleted", + error: (error) => `Failed to delete container: ${error}`, + }, + ) + } + > + Delete sessions + + + + ) : null; + }, + cell: ({ row }) => { + const session = row.original; + return ( + + + + + + Actions + navigator.clipboard.writeText(session.id)}> + Copy session ID + + + + toast.promise(() => manageSession(session.id, "stop", true), { + loading: "Stopping container...", + success: "Session stopped", + error: (error) => `Failed to stop container: ${error}`, + }) + } + > + Stop session + + + toast.promise(() => deleteSession(session.id, true), { + loading: "Deleting session...", + success: "Session deleted", + error: (error) => `Failed to delete container: ${error}`, + }) + } + > + Delete session + + + + ); + }, + }, +]; diff --git a/src/app/(main)/admin/sessions/page.tsx b/src/app/(main)/admin/sessions/page.tsx new file mode 100644 index 0000000..95964a5 --- /dev/null +++ b/src/app/(main)/admin/sessions/page.tsx @@ -0,0 +1,19 @@ +import { db } from "@/lib/drizzle/db"; +import { columns } from "./columns"; +import { DataTable } from "@/components/ui/data-table"; + +export default async function AdminPage() { + const sessions = await db.query.session.findMany({ + with: { + user: true, + }, + }); + return ( +
+

Sessions

+
+ +
+
+ ); +} diff --git a/src/app/(main)/admin/users/columns.tsx b/src/app/(main)/admin/users/columns.tsx new file mode 100644 index 0000000..e70f77d --- /dev/null +++ b/src/app/(main)/admin/users/columns.tsx @@ -0,0 +1,21 @@ +import type { SelectUserRelation } from "@/lib/drizzle/relational-types"; +import type { ColumnDef } from "@tanstack/react-table"; + +export const columns: ColumnDef[] = [ + { + accessorKey: "email", + header: "Email", + }, + { + accessorKey: "name", + header: "name", + }, + { + accessorKey: "id", + header: "User ID", + }, + { + accessorKey: "isAdmin", + header: "Admin", + }, +]; diff --git a/src/app/(main)/admin/users/page.tsx b/src/app/(main)/admin/users/page.tsx new file mode 100644 index 0000000..2caed59 --- /dev/null +++ b/src/app/(main)/admin/users/page.tsx @@ -0,0 +1,19 @@ +import { db } from "@/lib/drizzle/db"; +import { columns } from "./columns"; +import { DataTable } from "@/components/ui/data-table"; + +export default async function AdminPage() { + const sessions = await db.query.user.findMany({ + with: { + session: true, + }, + }); + return ( +
+

Users

+
+ +
+
+ ); +} diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index ee2ee2b..fb713e8 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -2,152 +2,16 @@ import Navigation from "@/components/navbar"; import { getAuthSession } from "@/lib/auth"; import { db, user as userSchema } from "@/lib/drizzle/db"; import { eq } from "drizzle-orm"; -import { Info, Book, Globe, Sparkles } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import packageJson from "@/../package.json"; -import Link from "next/link"; export default async function DashboardLayout({ children }: { children: React.ReactNode }) { const userSession = await getAuthSession(); - const dbUser = ( - await db - .select() - .from(userSchema) - .where(eq(userSchema.email, userSession?.user?.email as string)) - )[0]; - const projectsUsed = [ - { - name: "noVNC", - url: "https://github.com/noVNC/noVNC", - }, - { - name: "shadcn/ui", - url: "https://ui.shadcn.com/", - }, - ]; - const developers = ["incognitotgt", "yosoof3"]; + const [dbUser] = await db + .select() + .from(userSchema) + .where(eq(userSchema.email, userSession?.user?.email as string)); return (
{children} -
- - - - - - - - - About Stardust - - - - - - - Stardust {packageJson.version} - - - - Stardust is the platform for streaming isolated desktop containers. -
- Stardust uses the following things: -
- - Developers: -
- - Copyright 2024 Spaceness. -
- This version of Stardust was built on {new Date(Number(process.env.BUILD_DATE)).toLocaleString()} on - commit{" "} - - {process.env.GIT_COMMIT?.slice(0, 7)} - -
- - - - - - - Github - - - - - - Documentation - - - - - - Spaceness - - - -
-
-
-
); } diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index 44bb607..013496e 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -11,22 +11,23 @@ import { } from "@/components/ui/dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { CreateSessionButton } from "@/components/create-session-button"; +import { SessionDate } from "./session-date"; +import { SkeletionImage } from "@/components/skeleton-image"; import { AspectRatio } from "@/components/ui/aspect-ratio"; import { Button } from "@/components/ui/button"; import { Card, CardTitle } from "@/components/ui/card"; import { getAuthSession } from "@/lib/auth"; import docker from "@/lib/docker"; -import type Dockerode from "dockerode"; import { type SelectSession, db, image, user } from "@/lib/drizzle/db"; -import { createSession, deleteSession, manageSession } from "@/lib/session"; +import { deleteSession, manageSession } from "@/lib/session"; +import type Dockerode from "dockerode"; import { eq } from "drizzle-orm"; import { Container, Loader2, Pause, Play, Square, TrashIcon } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; import { redirect } from "next/navigation"; import { Suspense } from "react"; -import { SkeletionImage } from "@/components/skeleton-image"; -import { CreateSessionButton } from "@/components/create-session-button"; const errorCatcher = (error: string) => { throw new Error(error); }; @@ -55,28 +56,32 @@ const ManageSessionButton = ({ ); export default async function Dashboard() { const userSession = await getAuthSession(); - const images = await db.select().from(image).catch(errorCatcher); - const { userId } = ( - await db + const { sessions, images } = await db.transaction(async (tx) => { + const [{ userId }] = await tx .select({ userId: user.id, }) .from(user) .where(eq(user.email, userSession?.user?.email as string)) - .catch(errorCatcher) - )[0]; - const sessions = await db.query.session - .findMany({ - with: { - image: true, - }, - where: (users, { eq }) => eq(users.userId, userId), - }) - .catch(errorCatcher); + .catch(errorCatcher); + const images = await db.select().from(image).catch(errorCatcher); + const sessions = await tx.query.session + .findMany({ + with: { + image: true, + }, + where: (users, { eq }) => eq(users.userId, userId), + }) + .catch(errorCatcher); + return { sessions, images }; + }); return (
- + Workspaces Sessions @@ -149,10 +154,7 @@ export default async function Dashboard() {

{session.id.slice(0, 6)}

-

- Expires at{" "} - {`${expiresAt.toLocaleTimeString()} on ${expiresAt.getMonth()}/${expiresAt.getDate()}/${expiresAt.getFullYear()}`} -

+
{!State.Paused && State.Running ? ( @@ -160,6 +162,7 @@ export default async function Dashboard() { @@ -171,11 +174,11 @@ export default async function Dashboard() { )}
{!State.Paused && State.Running ? ( - + ) : null} {State.Running ? ( +

+ Expires at {`${expiresAt.toLocaleTimeString()} on ${expiresAt.toLocaleDateString("en-US")}`} + {hydrated ? "" : " (UTC)"} +

+ + ); +} diff --git a/src/app/api/session/[slug]/files/route.ts b/src/app/api/session/[slug]/files/route.ts index c297135..3e2c72e 100644 --- a/src/app/api/session/[slug]/files/route.ts +++ b/src/app/api/session/[slug]/files/route.ts @@ -1,14 +1,16 @@ import { getAuthSession } from "@/lib/auth"; import { getSession } from "@/lib/session/get-session"; +import { sessionRunning } from "@/lib/session/session-running"; import type { NextRequest } from "next/server"; export async function GET(req: NextRequest, { params }: { params: { slug: string } }) { const userSession = await getAuthSession(); const fileName = req.nextUrl.searchParams.get("name"); const { id, agentPort } = (await getSession(params.slug, userSession)) || {}; - if (!id) { + if (!id || !agentPort) { return Response.json(["not found"], { status: 404 }); } + await sessionRunning(agentPort); if (fileName) { try { const download = await fetch(`http://${process.env.CONTAINER_HOST}:${agentPort}/files/download/${fileName}`); diff --git a/src/app/api/session/[slug]/route.ts b/src/app/api/session/[slug]/route.ts new file mode 100644 index 0000000..2156131 --- /dev/null +++ b/src/app/api/session/[slug]/route.ts @@ -0,0 +1,31 @@ +import { getAuthSession } from "@/lib/auth"; +import docker from "@/lib/docker"; +import { getSession } from "@/lib/session/get-session"; +import { sessionRunning } from "@/lib/session/session-running"; +import type { NextRequest } from "next/server"; + +export async function GET(_req: NextRequest, { params }: { params: { slug: string } }) { + const userSession = await getAuthSession(); + const containerSession = await getSession(params.slug, userSession); + if (!containerSession) { + return Response.json({ exists: false, error: "Container not found" }, { status: 404 }); + } + const container = docker.getContainer(containerSession.id); + const { State } = await container.inspect(); + if (!State.Running) { + await container.start(); + } + if (State.Paused) { + await container.unpause(); + } + await sessionRunning(containerSession.agentPort); + const password = await fetch(`http://${process.env.CONTAINER_HOST}:${containerSession.agentPort}/password`).then( + (res) => res.text(), + ); + return Response.json({ + exists: true, + password, + url: `/websockify/${containerSession.id}`, + }); +} +export const dynamic = "force-dynamic"; diff --git a/src/app/api/vnc/[slug]/route.ts b/src/app/api/vnc/[slug]/route.ts deleted file mode 100644 index 580c745..0000000 --- a/src/app/api/vnc/[slug]/route.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { getAuthSession } from "@/lib/auth"; -import docker from "@/lib/docker"; -import { getSession as getContainerSession } from "@/lib/session/get-session"; -import type { IncomingMessage } from "node:http"; -import net from "node:net"; -import { getSession } from "next-auth/react"; -import type { NextRequest } from "next/server"; -import type { WebSocket, WebSocketServer } from "ws"; -import { sessionRunning } from "@/lib/session/session-running"; - -export async function GET(_req: NextRequest, { params }: { params: { slug: string } }) { - const userSession = await getAuthSession(); - const containerSession = await getContainerSession(params.slug, userSession); - if (!containerSession) { - return Response.json({ exists: false, error: "Container not found" }, { status: 404 }); - } - const container = docker.getContainer(containerSession.id); - const { State } = await container.inspect(); - if (!State.Running) { - await container.start(); - } - await sessionRunning(containerSession.agentPort); - const password = await fetch(`http://${process.env.CONTAINER_HOST}:${containerSession.agentPort}/password`).then( - (res) => res.text(), - ); - return Response.json({ exists: true, paused: State.Paused, password, url: `/api/vnc/${containerSession.id}` }); -} - -export async function SOCKET(ws: WebSocket, req: IncomingMessage, _server: WebSocketServer) { - const containerId = req.url?.split("/")[3]; - if (!containerId) { - ws.close(); - return; - } - const userSession = await getSession({ req }); - if (!userSession || !userSession.user) { - ws.close(); - return; - } - const containerSession = await getContainerSession(containerId, userSession); - if (!containerSession) { - ws.close(); - return; - } - const tcpSocket = net.connect(containerSession.vncPort, process.env.CONTAINER_HOST as string); - ws.on("message", (message: Uint8Array) => { - tcpSocket.write(message); - }); - ws.on("close", (code, reason) => { - console.log(`✨ Stardust INFO: Connection closed due to ${reason} with code ${code}`); - tcpSocket.end(); - }); - - tcpSocket.on("data", (data) => { - ws.send(data); - }); - - tcpSocket.on("error", (err) => { - console.error(err); - ws.close(); - }); - - tcpSocket.on("close", () => { - ws.close(); - }); -} diff --git a/src/app/icon.svg b/src/app/icon.svg index 0b1b892..4d4dd3c 100644 --- a/src/app/icon.svg +++ b/src/app/icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/app/manifest.ts b/src/app/manifest.ts new file mode 100644 index 0000000..a99305c --- /dev/null +++ b/src/app/manifest.ts @@ -0,0 +1,20 @@ +import type { MetadataRoute } from "next"; + +export default function manifest(): MetadataRoute.Manifest { + return { + name: "Stardust", + short_name: "Stardust", + description: "Stardust is the platform for streaming isolated desktop containers.", + start_url: "/", + display: "standalone", + background_color: "#09090b", + theme_color: "#09090b", + icons: [ + { + src: "/icon.svg", + sizes: "any", + type: "image/x-icon", + }, + ], + }; +} diff --git a/src/app/view/[slug]/page.tsx b/src/app/view/[slug]/page.tsx index 1e7839f..aed01b6 100644 --- a/src/app/view/[slug]/page.tsx +++ b/src/app/view/[slug]/page.tsx @@ -1,6 +1,5 @@ "use client"; -import { deleteSession, manageSession } from "@/lib/session"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { AlertDialog, @@ -14,6 +13,7 @@ import { AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { deleteSession, manageSession } from "@/lib/session"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; @@ -30,6 +30,7 @@ import type { VncViewerHandle } from "@/components/vnc-screen"; import { fetcher } from "@/lib/utils"; import { AlertCircle, + Camera, ChevronRight, Clipboard, Download, @@ -43,14 +44,15 @@ import { RotateCw, ScreenShareOff, Sparkles, + Square, TrashIcon, Upload, } from "lucide-react"; -import { notFound, useRouter } from "next/navigation"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; import { Suspense, lazy, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; -import useSWR, { useSWRConfig } from "swr"; -import Link from "next/link"; +import useSWR from "swr"; type ScalingValues = "remote" | "local" | "none"; const Loading = ({ text }: { text: string }) => ( @@ -61,17 +63,9 @@ const Loading = ({ text }: { text: string }) => ( const VncScreen = lazy(() => import("@/components/vnc-screen")); export default function View({ params }: { params: { slug: string } }) { const vncRef = useRef(null); - const [session, setSession] = useState<{ - exists: boolean; - url: string | null; - error?: string; - paused?: boolean; - password?: string; - } | null>(null); const [connected, setConnected] = useState(false); const [fullScreen, setFullScreen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false); - const [accordionValue, setAccordionValue] = useState(undefined); const [workingClipboard, setWorkingClipboard] = useState(true); // noVNC options start const [clipboard, setClipboard] = useState(""); @@ -82,21 +76,28 @@ export default function View({ params }: { params: { slug: string } }) { const [scaling, setScaling] = useState("remote"); // noVNC options end const router = useRouter(); - const { mutate } = useSWRConfig(); + const { + data: session, + error: sessionError, + isLoading: sessionLoading, + mutate: sessionMutate, + } = useSWR<{ + exists: boolean; + url: string | null; + error?: string; + password?: string; + } | null>(`/api/session/${params.slug}`, fetcher, { + onErrorRetry: (error, _key, _config, revalidate, { retryCount }) => { + if (error.status === 404) return; + setTimeout(() => revalidate({ retryCount }), 1000); + }, + }); const { data: filesList, error: filesError, isLoading: filesLoading, + mutate: filesMutate, } = useSWR(`/api/session/${params.slug}/files`, fetcher, { refreshInterval: 10000 }); - useEffect(() => { - if (!session) - fetch(`/api/vnc/${params.slug}`) - .then((res) => res.json()) - .then((data) => { - setSession(data); - }) - .catch(() => {}); - }, [session, params.slug]); useEffect(() => { if (connected && vncRef.current?.rfb) { vncRef.current.rfb.viewOnly = viewOnly; @@ -107,10 +108,14 @@ export default function View({ params }: { params: { slug: string } }) { vncRef.current.rfb.scaleViewport = scaling === "local"; } }, [connected, viewOnly, qualityLevel, compressionLevel, scaling, clipViewport]); + // why did i even use swr for this raaaaaaah + useEffect(() => { + if (!session) sessionMutate(); + }, [session, sessionMutate]); // jank, but it works ¯\_(ツ)_/¯ useEffect(() => { const interval = setInterval(() => { - if (connected && vncRef.current?.rfb && document.hasFocus()) { + if (connected && vncRef.current?.rfb && document.hasFocus() && !sidebarOpen) { vncRef.current.rfb.focus(); navigator.clipboard .readText() @@ -125,13 +130,13 @@ export default function View({ params }: { params: { slug: string } }) { } }, 250); return () => clearInterval(interval); - }, [connected, clipboard]); + }, [connected, clipboard, sidebarOpen]); useEffect(() => { const interval = setInterval(() => { if (connected && document.hasFocus()) fetch(`/api/session/${params.slug}/keepalive`, { method: "POST" }); }, 10000); return () => clearInterval(interval); - }); + }, [connected, params.slug]); useEffect(() => { if (document.fullscreenElement === null && fullScreen) { document.documentElement.requestFullscreen(); @@ -140,7 +145,7 @@ export default function View({ params }: { params: { slug: string } }) { } }, [fullScreen]); return ( -
+
{connected ? ( + + + - +
@@ -282,11 +303,9 @@ export default function View({ params }: { params: { slug: string } }) { spellCheck={false} autoCorrect="off" autoCapitalize="off" - value={!workingClipboard ? clipboard : undefined} + value={clipboard} disabled={workingClipboard} - placeholder={ - workingClipboard ? "Clipboard is already synced" : "Paste here to send to remote machine" - } + placeholder="Paste here to send to remote machine" onChange={(e) => { if (vncRef.current?.rfb) { setClipboard(e.target.value); @@ -393,7 +412,7 @@ export default function View({ params }: { params: { slug: string } }) { { loading: "Uploading file...", success: "File uploaded", - error: "Failed to upload file", + error: (error) => `Failed to upload file: ${error.message}`, }, ); }); @@ -458,67 +477,62 @@ export default function View({ params }: { params: { slug: string } }) {
- {session ? ( - session.exists ? ( - session.url && session.password ? ( - !session.paused ? ( - }> - } - onClipboard={(e) => { - if (e?.detail.text) { - setClipboard(e.detail.text); - navigator.clipboard.writeText(e.detail.text).catch(() => setWorkingClipboard(false)); - } - }} - onConnect={() => { - setConnected(true); - toast.success(`Connected to session ${params.slug.slice(0, 6)}`); - }} - onDisconnect={() => { - setConnected(false); - setSidebarOpen(false); - }} - onSecurityFailure={() => { - setSession(null); - setSidebarOpen(false); - }} - ref={vncRef} - rfbOptions={{ - credentials: { - username: "", - password: session.password, - target: "", - }, - }} - focusOnClick - className="absolute z-20 h-screen w-screen overflow-clip" - /> - - ) : ( -
-
-

Session Paused

-

This session is paused.

- -
-
- ) - ) : null + {!sessionError ? ( + !sessionLoading ? ( + session?.exists && session.url && session.password ? ( + }> + } + onClipboard={(e) => { + if (e?.detail.text) { + setClipboard(e.detail.text); + navigator.clipboard.writeText(e.detail.text).catch(() => setWorkingClipboard(false)); + } + }} + onConnect={() => { + setConnected(true); + toast.success(`Connected to session ${params.slug.slice(0, 6)}`); + }} + onDisconnect={() => { + setConnected(false); + setSidebarOpen(false); + }} + onSecurityFailure={() => sessionMutate(null)} + ref={vncRef} + rfbOptions={{ + credentials: { + username: "", + password: session.password, + target: "", + }, + }} + focusOnClick + className="absolute z-20 h-screen w-screen overflow-clip" + /> + + ) : ( + + + Session not found, or loading + + Container might be restarting. If this screen doesn't go away, the session might not exist. + + + ) ) : ( - notFound() + ) ) : ( - + + + Error + + Failed to get session data +
+ {sessionError.message} +
+
)}
); diff --git a/src/components/create-session-button.tsx b/src/components/create-session-button.tsx index 5003d91..8e6b31e 100644 --- a/src/components/create-session-button.tsx +++ b/src/components/create-session-button.tsx @@ -1,9 +1,9 @@ "use client"; -import { useTransition } from "react"; -import { Button } from "./ui/button"; -import { toast } from "sonner"; import { createSession } from "@/lib/session"; import { useRouter } from "next/navigation"; +import { useTransition } from "react"; +import { toast } from "sonner"; +import { Button } from "./ui/button"; export function CreateSessionButton({ image }: { image: string }) { const [isPending, startTransition] = useTransition(); const router = useRouter(); @@ -12,17 +12,15 @@ export function CreateSessionButton({ image }: { image: string }) { disabled={isPending} onClick={() => startTransition(async () => { - const toastId = toast.loading("Creating session..."); await createSession(image) .then((session) => { if (!session) { throw new Error("Container not created"); } - toast.success("Session created", { id: toastId }); router.push(`/view/${session[0].id}`); }) .catch((err) => { - toast.error(err.message, { id: toastId }); + toast.error(err.message); }); }) } diff --git a/src/components/data-table/column-header.tsx b/src/components/data-table/column-header.tsx new file mode 100644 index 0000000..88dfae4 --- /dev/null +++ b/src/components/data-table/column-header.tsx @@ -0,0 +1,55 @@ +import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDown } from "lucide-react"; +import type { Column } from "@tanstack/react-table"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +interface DataTableColumnHeaderProps extends React.HTMLAttributes { + column: Column; + title: string; +} + +export function DataTableColumnHeader({ + column, + title, + className, +}: DataTableColumnHeaderProps) { + if (!column.getCanSort()) { + return
{title}
; + } + + return ( +
+ + + + + + column.toggleSorting(false)}> + + Asc + + column.toggleSorting(true)}> + + Desc + + + +
+ ); +} diff --git a/src/components/data-table/column-toggle.tsx b/src/components/data-table/column-toggle.tsx new file mode 100644 index 0000000..f2154d9 --- /dev/null +++ b/src/components/data-table/column-toggle.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; +import { SlidersHorizontal } from "lucide-react"; +import type { Table } from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; + +interface DataTableViewOptionsProps { + table: Table; +} + +export function DataTableViewOptions({ table }: DataTableViewOptionsProps) { + return ( + + + + + + Toggle columns + + {table + .getAllColumns() + .filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide()) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ); + })} + + + ); +} diff --git a/src/components/data-table/pagination.tsx b/src/components/data-table/pagination.tsx new file mode 100644 index 0000000..7e236ec --- /dev/null +++ b/src/components/data-table/pagination.tsx @@ -0,0 +1,82 @@ +import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeft, ChevronsRight } from "lucide-react"; +import type { Table } from "@tanstack/react-table"; + +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +interface DataTablePaginationProps { + table: Table; +} + +export function DataTablePagination({ table }: DataTablePaginationProps) { + return ( +
+
+ {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+
+

Rows per page

+ +
+
+ Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()} +
+
+ + + + +
+
+
+ ); +} diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index e2d3054..dc59772 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -1,17 +1,26 @@ "use client"; - +import packageJson from "@/../package.json"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, + DropdownMenuPortal, DropdownMenuSeparator, - DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, - DropdownMenuPortal, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { NavigationMenu, @@ -20,17 +29,19 @@ import { NavigationMenuList, navigationMenuTriggerStyle, } from "@/components/ui/navigation-menu"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import type { SelectUser } from "@/lib/drizzle/schema"; import { cn } from "@/lib/utils"; -import { ComputerIcon, HelpCircle, LogOut, Settings, Sparkles, SwatchBook, User } from "lucide-react"; +import { Book, ComputerIcon, Globe, Info, LogOut, Settings, Sparkles, SwatchBook, User } from "lucide-react"; import type { Route } from "next"; import type { Session } from "next-auth"; -import Link from "next/link"; -import { Fragment } from "react"; import { useTheme } from "next-themes"; +import Link from "next/link"; +import { Fragment, useState } from "react"; export default function Navigation({ dbUser, session }: { dbUser: SelectUser; session: Session | null }) { const { name, email, image } = session?.user || {}; + const [open, setDialogOpen] = useState(false); const { themes, setTheme } = useTheme(); const navigationItems: { icon: React.ReactNode; @@ -51,10 +62,21 @@ export default function Navigation({ dbUser, session }: { dbUser: SelectUser; se adminOnly: true, }, ]; + const projectsUsed = [ + { + name: "noVNC", + url: "https://github.com/noVNC/noVNC", + }, + { + name: "shadcn/ui", + url: "https://ui.shadcn.com/", + }, + ]; + const developers = ["incognitotgt", "yosoof3"]; return ( -