From 9d7dcbfb9e390d02eba5a36930c59d0d5c73634c Mon Sep 17 00:00:00 2001 From: incognitotgt Date: Tue, 23 Apr 2024 07:15:33 -0400 Subject: [PATCH] change formatter file upload --- .eslintrc.json | 3 - .gitignore | 1 + biome.json | 18 +- config.json | 2 +- drizzle.config.ts | 8 +- next.config.mjs | 23 +- package.json | 5 +- pnpm-lock.yaml | 84 ++++- postcss.config.cjs | 2 +- src/actions/client-session.ts | 34 +- src/app/(main)/admin/layout.tsx | 16 +- src/app/(main)/admin/page.tsx | 2 +- src/app/(main)/error.tsx | 10 +- src/app/(main)/layout.tsx | 14 +- src/app/(main)/page.tsx | 417 +++++++++++----------- src/app/api/auth/[...nextauth]/route.ts | 8 +- src/app/api/session/files/[slug]/route.ts | 70 ++++ src/app/api/session/preview/route.ts | 33 +- src/app/api/vnc/[slug]/route.ts | 77 ++-- src/app/auth/error/page.tsx | 14 +- src/app/auth/layout.tsx | 8 +- src/app/auth/login/page.tsx | 32 +- src/app/auth/logout/page.tsx | 30 +- src/app/auth/verify/page.tsx | 14 +- src/app/globals.css | 333 ++++++++--------- src/app/layout.tsx | 33 +- src/app/not-found.tsx | 8 +- src/app/providers.tsx | 6 +- src/app/view/[slug]/page.tsx | 337 +++++++++++------ src/app/view/error.tsx | 8 +- src/app/view/not-found.tsx | 2 +- src/components/mode-toggle.tsx | 16 +- src/components/navbar.tsx | 99 +++-- src/components/submit-button.tsx | 12 +- src/components/ui/alert-dialog.tsx | 50 +-- src/components/ui/alert.tsx | 22 +- src/components/ui/aspect-ratio.tsx | 7 + src/components/ui/avatar.tsx | 22 +- src/components/ui/button.tsx | 22 +- src/components/ui/card.tsx | 30 +- src/components/ui/context-menu.tsx | 68 ++-- src/components/ui/dialog.tsx | 44 +-- src/components/ui/dropdown-menu.tsx | 68 ++-- src/components/ui/input.tsx | 22 ++ src/components/ui/label.tsx | 18 +- src/components/ui/navigation-menu.tsx | 42 +-- src/components/ui/popover.tsx | 18 +- src/components/ui/select.tsx | 46 +-- src/components/ui/separator.tsx | 14 +- src/components/ui/sheet.tsx | 48 +-- src/components/ui/skeleton.tsx | 6 +- src/components/ui/slider.tsx | 14 +- src/components/ui/sonner.tsx | 16 +- src/components/ui/switch.tsx | 14 +- src/components/ui/tabs.tsx | 55 +++ src/components/ui/textarea.tsx | 12 +- src/components/ui/tooltip.tsx | 20 +- src/components/vnc-screen.tsx | 360 ++++++++++--------- src/lib/auth.ts | 18 +- src/lib/docker.ts | 14 +- src/lib/drizzle/db.ts | 12 +- src/lib/drizzle/schema.ts | 22 +- src/lib/drizzle/seed.ts | 30 +- src/lib/migrate.ts | 10 +- src/lib/util/get-session.ts | 18 +- src/lib/util/session.ts | 70 ++-- src/lib/utils.ts | 7 +- src/middleware.ts | 6 +- tailwind.config.ts | 9 +- 69 files changed, 1704 insertions(+), 1329 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 src/app/api/session/files/[slug]/route.ts create mode 100644 src/components/ui/aspect-ratio.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/tabs.tsx diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 72cc705..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/.gitignore b/.gitignore index 1b924b6..93e3209 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ yarn-error.log* next-env.d.ts .tmp docker/ +.assets diff --git a/biome.json b/biome.json index 20e59cb..3f3840b 100644 --- a/biome.json +++ b/biome.json @@ -2,22 +2,30 @@ "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json", "formatter": { "enabled": true, - "formatWithErrors": false, + "formatWithErrors": true, "indentStyle": "tab", "indentWidth": 2, "lineEnding": "lf", "lineWidth": 120, "attributePosition": "auto", - "ignore": ["**/pnpm-lock.yaml", "**/.next/**"] + "ignore": ["**/pnpm-lock.yaml", "**/.next/**", "**/node_modules/**"] + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + }, + "ignore": ["**/pnpm-lock.yaml", "**/.next/**", "**/node_modules/**"] }, - "organizeImports": { "enabled": true }, - "linter": { "enabled": true, "rules": { "recommended": true }, "ignore": ["**/pnpm-lock.yaml", "**/.next/**"] }, "javascript": { "formatter": { "jsxQuoteStyle": "double", "quoteProperties": "asNeeded", "trailingComma": "all", - "semicolons": "asNeeded", + "semicolons": "always", "arrowParentheses": "always", "bracketSpacing": true, "bracketSameLine": false, diff --git a/config.json b/config.json index 97e55e9..3e26ded 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { "style": { - "backgroundImage": "https://raw.githubusercontent.com/zhichaoh/catppuccin-wallpapers/main/waves/cat-waves.png" + "backgroundImage": "https://raw.githubusercontent.com/zhichaoh/catppuccin-wallpapers/main/landscapes/yosemite.png" } } diff --git a/drizzle.config.ts b/drizzle.config.ts index 3151e28..637c02b 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,5 +1,5 @@ -import "dotenv/config" -import type { Config } from "drizzle-kit" +import "dotenv/config"; +import type { Config } from "drizzle-kit"; const config: Config = { driver: "pg", @@ -10,5 +10,5 @@ const config: Config = { }, verbose: true, strict: true, -} -export default config +}; +export default config; diff --git a/next.config.mjs b/next.config.mjs index 5e8e878..b95cdaa 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,26 +1,21 @@ // @ts-check -import { verifyPatch } from "next-ws/server/index.js" +import { verifyPatch } from "next-ws/server/index.js"; -verifyPatch() +verifyPatch(); /** @type {import('next').NextConfig} */ const nextConfig = { images: { remotePatterns: [ { protocol: "https", - hostname: "http.cat", - port: "", - pathname: "/**", - }, - { - protocol: "https", - hostname: "raw.githubusercontent.com", + hostname: "*", port: "", pathname: "/**", }, ], }, experimental: { + typedRoutes: true, serverActions: { allowedOrigins: ["localhost:3000", "*.use.devtunnels.ms"], }, @@ -32,15 +27,15 @@ const nextConfig = { destination: "/auth/error", permanent: true, }, - ] + ]; }, webpack(config) { config.module.rules.push({ test: /\.node$/, loader: "node-loader", - }) - return config + }); + return config; }, -} +}; -export default nextConfig +export default nextConfig; diff --git a/package.json b/package.json index d39b9b1..b5d122b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@novnc/novnc": "^1.4.0", "@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-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", @@ -28,6 +29,7 @@ "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.7", "@types/dockerode": "^3.3.26", "class-variance-authority": "^0.7.0", @@ -48,6 +50,7 @@ "react-dom": "^18.2.0", "server-only": "^0.0.1", "sonner": "^1.4.41", + "swr": "^2.2.5", "tailwind-merge": "^2.2.2", "tailwindcss-animate": "^1.0.7", "ws": "^8.16.0" @@ -61,7 +64,7 @@ "@types/react-dom": "^18.2.22", "@types/ws": "^8.5.10", "autoprefixer": "^10.4.19", - "drizzle-kit": "^0.20.16", + "drizzle-kit": "^0.20.17", "postcss": "^8.4.38", "tailwindcss": "^3.4.1", "tailwindcss-dotted-background": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11bf071..ec01a7e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@radix-ui/react-alert-dialog': specifier: ^1.0.5 version: 1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-aspect-ratio': + specifier: ^1.0.3 + version: 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-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) @@ -47,6 +50,9 @@ dependencies: '@radix-ui/react-switch': specifier: ^1.0.3 version: 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-tabs': + 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-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) @@ -107,6 +113,9 @@ dependencies: sonner: specifier: ^1.4.41 version: 1.4.41(react-dom@18.2.0)(react@18.2.0) + swr: + specifier: ^2.2.5 + version: 2.2.5(react@18.2.0) tailwind-merge: specifier: ^2.2.2 version: 2.2.2 @@ -143,8 +152,8 @@ devDependencies: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.38) drizzle-kit: - specifier: ^0.20.16 - version: 0.20.16 + specifier: ^0.20.17 + version: 0.20.17 postcss: specifier: ^8.4.38 version: 8.4.38 @@ -921,6 +930,27 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-aspect-ratio@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-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==} + 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/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) + '@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-avatar@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-kVK2K7ZD3wwj3qhle0ElXhOjbezIgyl2hVvgwfIdexL3rN6zJmy5AqqIf+D31lxVppdzV8CjAfZ6PklkmInZLw==} peerDependencies: @@ -1561,6 +1591,34 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-tabs@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-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==} + 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-context': 1.0.1(@types/react@18.2.67)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.67)(react@18.2.0) + '@radix-ui/react-id': 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-roving-focus': 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-use-controllable-state': 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-tooltip@1.0.7(@types/react-dom@18.2.22)(@types/react@18.2.67)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==} peerDependencies: @@ -2326,8 +2384,8 @@ packages: wordwrap: 1.0.0 dev: true - /drizzle-kit@0.20.16: - resolution: {integrity: sha512-WoqV0XDny8mHnVoLYcUs6H3Ae1HBxeV0zi3VXG/mTahIQYfadZmIc3eRqzXjD3yxemj+GdHI1nThddmsoabdIA==} + /drizzle-kit@0.20.17: + resolution: {integrity: sha512-mLVDS4nXmO09wFVlzGrdshWnAL+U9eQGC5zRs6hTN6Q9arwQGWU2XnZ17I8BM8Quau8CQRx3Ms6VPgRWJFVp7Q==} hasBin: true dependencies: '@esbuild-kit/esm-loader': 2.6.5 @@ -3823,6 +3881,16 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + /swr@2.2.5(react@18.2.0): + resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + client-only: 0.0.1 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /tailwind-merge@2.2.2: resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==} dependencies: @@ -4031,6 +4099,14 @@ packages: tslib: 2.6.2 dev: false + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} diff --git a/postcss.config.cjs b/postcss.config.cjs index 1b69d43..e873f1a 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -3,4 +3,4 @@ module.exports = { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/src/actions/client-session.ts b/src/actions/client-session.ts index 274d14e..4937d91 100644 --- a/src/actions/client-session.ts +++ b/src/actions/client-session.ts @@ -1,21 +1,21 @@ -"use server" -import { getAuthSession } from "@/lib/auth" -import { deleteSession as deleteSessionBase, manageSession as manageSessionBase } from "@/lib/util/session" -import type Dockerode from "dockerode" -import { revalidatePath } from "next/cache" -import { redirect } from "next/navigation" +"use server"; +import { getAuthSession } from "@/lib/auth"; +import { deleteSession as deleteSessionBase, manageSession as manageSessionBase } from "@/lib/util/session"; +import type Dockerode from "dockerode"; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; async function deleteSession(id: string) { - const userSession = await getAuthSession() - if (!userSession || !userSession.user) return - await deleteSessionBase(id, userSession) - revalidatePath("/") - redirect("/") + const userSession = await getAuthSession(); + if (!userSession || !userSession.user) return; + await deleteSessionBase(id, userSession); + revalidatePath("/"); + redirect("/"); } async function manageSession(id: string, action: keyof Dockerode.Container, navigate = true) { - const userSession = await getAuthSession() - if (!userSession || !userSession.user) return - await manageSessionBase(id, action, userSession) - revalidatePath("/") - if (navigate) redirect("/") + const userSession = await getAuthSession(); + if (!userSession || !userSession.user) return; + await manageSessionBase(id, action, userSession); + revalidatePath("/"); + if (navigate) redirect("/"); } -export { deleteSession, manageSession } +export { deleteSession, manageSession }; diff --git a/src/app/(main)/admin/layout.tsx b/src/app/(main)/admin/layout.tsx index c18d01a..c4115ff 100644 --- a/src/app/(main)/admin/layout.tsx +++ b/src/app/(main)/admin/layout.tsx @@ -1,18 +1,18 @@ -import { getAuthSession } from "@/lib/auth" -import { db, user } from "@/lib/drizzle/db" -import { eq } from "drizzle-orm" -import { redirect } from "next/navigation" +import { getAuthSession } from "@/lib/auth"; +import { db, user } from "@/lib/drizzle/db"; +import { eq } from "drizzle-orm"; +import { redirect } from "next/navigation"; export default async function AdminLayout({ children }: { children: React.ReactNode }) { - const userSession = await getAuthSession() + const userSession = await getAuthSession(); const { isAdmin } = ( await db .select({ isAdmin: user.isAdmin }) .from(user) .where(eq(user.email, userSession?.user?.email as string)) - )[0] + )[0]; if (!isAdmin) { - return redirect("/") + return redirect("/"); } - return children + return children; } diff --git a/src/app/(main)/admin/page.tsx b/src/app/(main)/admin/page.tsx index fac0528..8292683 100644 --- a/src/app/(main)/admin/page.tsx +++ b/src/app/(main)/admin/page.tsx @@ -1,3 +1,3 @@ export default function AdminPage() { - return
coming soon (hopefully)
+ return
coming soon (hopefully)
; } diff --git a/src/app/(main)/error.tsx b/src/app/(main)/error.tsx index c40aef1..87f792b 100644 --- a/src/app/(main)/error.tsx +++ b/src/app/(main)/error.tsx @@ -1,7 +1,7 @@ -"use client" +"use client"; -import { Button } from "@/components/ui/button" -import { Sparkles } from "lucide-react" +import { Button } from "@/components/ui/button"; +import { Sparkles } from "lucide-react"; // biome-ignore lint: lint/suspicious/noShadowRestrictedNames export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { return ( @@ -22,10 +22,10 @@ export default function Error({ error, reset }: { error: Error & { digest?: stri Reset

- More details are in the {typeof window === "undefined" ? "terminal" : "browser"} console + More details are in the {typeof window === "undefined" ? "server logs" : "browser console"}

- ) + ); } diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 20f2c7d..b3460fa 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -1,19 +1,19 @@ -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 Navigation from "@/components/navbar"; +import { getAuthSession } from "@/lib/auth"; +import { db, user as userSchema } from "@/lib/drizzle/db"; +import { eq } from "drizzle-orm"; export default async function DashboardLayout({ children }: { children: React.ReactNode }) { - const userSession = await getAuthSession() + const userSession = await getAuthSession(); const dbUser = ( await db .select() .from(userSchema) .where(eq(userSchema.email, userSession?.user?.email as string)) - )[0] + )[0]; return (
{children}
- ) + ); } diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index 6a5106e..36ab769 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -1,4 +1,4 @@ -import { StyledSubmit } from "@/components/submit-button" +import { StyledSubmit } from "@/components/submit-button"; import { Dialog, DialogClose, @@ -8,27 +8,29 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog" +} from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Button } from "@/components/ui/button" -import { Card } from "@/components/ui/card" -import { getAuthSession } from "@/lib/auth" -import docker from "@/lib/docker" -import { db, image, user } from "@/lib/drizzle/db" -import { createSession, deleteSession, manageSession } from "@/lib/util/session" -import { eq } from "drizzle-orm" -import { Container, Loader2, Pause, Play, Square, TrashIcon } from "lucide-react" -import { revalidatePath } from "next/cache" -import Image from "next/image" -import Link from "next/link" -import { redirect } from "next/navigation" -import { Suspense } from "react" +import { AspectRatio } from "@/components/ui/aspect-ratio"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { getAuthSession } from "@/lib/auth"; +import docker from "@/lib/docker"; +import { db, image, user } from "@/lib/drizzle/db"; +import { createSession, deleteSession, manageSession } from "@/lib/util/session"; +import { eq } from "drizzle-orm"; +import { Container, Loader2, Pause, Play, Square, TrashIcon } from "lucide-react"; +import { revalidatePath } from "next/cache"; +import Image from "next/image"; +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { Suspense } from "react"; const errorCatcher = (error: string) => { - throw new Error(error) -} + throw new Error(error); +}; export default async function Dashboard() { - const userSession = await getAuthSession() - const images = await db.select().from(image).catch(errorCatcher) + const userSession = await getAuthSession(); + const images = await db.select().from(image).catch(errorCatcher); const { userId } = ( await db .select({ @@ -37,7 +39,7 @@ export default async function Dashboard() { .from(user) .where(eq(user.email, userSession?.user?.email as string)) .catch(errorCatcher) - )[0] + )[0]; const sessions = await db.query.session .findMany({ with: { @@ -45,192 +47,209 @@ export default async function Dashboard() { }, where: (users, { eq }) => eq(users.userId, userId), }) - .catch(errorCatcher) + .catch(errorCatcher); return (
-
- }> - {images.map((image) => ( - - - - {image.friendlyName} -
-

{image.friendlyName}

-

{image.category}

-
-
-
- -
{ - "use server" - if (!userSession) return - const containerSession = await createSession(image.dockerImage, userSession).catch((e) => { - throw new Error(e) - }) - if (!containerSession) return - redirect(`/view/${containerSession[0].id}?nocheck=true`) - }} - > - - {image.friendlyName} - Launch a new {image.friendlyName} Session. - - - - - - Launch - -
-
-
- ))} -
-
- {sessions.length ? ( -
-
+ + + Workspaces + Sessions + + +
}> - {sessions - ? sessions.map(async (session) => { - const { State } = await docker.getContainer(session.id).inspect() - const expiresAt = new Date(session.expiresAt) - return ( - -
- {session.image.friendlyName} + {images.map((image) => ( + + + + {image.friendlyName} +
+

{image.friendlyName}

+

{image.category}

+
+
+
+ +
{ + "use server"; + if (!userSession) return; + const containerSession = await createSession(image.dockerImage, userSession).catch((e) => { + throw new Error(e); + }); + if (!containerSession) return; + redirect(`/view/${containerSession[0].id}?nocheck=true`); + }} + > + + Launch session + Start a new {image.friendlyName} session? + + + + + -
-

{session.image.friendlyName}

-

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

-

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

-
-
- {session.imagePreview ? ( - - ) : ( -
- -
- )} -
- {!State.Paused && State.Running ? ( - - - - - - ) : null} - {!State.Paused && State.Running ? ( - { - "use server" - if (!userSession) return - await manageSession(session.id, "pause", userSession).catch(errorCatcher) - revalidatePath("/") - }} - > - - - - - ) : State.Paused ? ( -
{ - "use server" - if (!userSession) return - await manageSession(session.id, "unpause", userSession).catch(errorCatcher) - redirect(`/view/${session.id}?nocheck=true`) - }} - > - - - -
- ) : null} - {!State.Running ? ( -
{ - "use server" - if (!userSession) return - await manageSession(session.id, "start", userSession).catch(errorCatcher) - redirect(`/view/${session.id}?nocheck=true`) - }} - > - - - -
- ) : State.Running ? ( -
{ - "use server" - if (!userSession) return - await manageSession(session.id, "stop", userSession).catch(errorCatcher) - revalidatePath("/") - }} - > - - - -
- ) : null} -
{ - "use server" - if (!userSession) return - await deleteSession(session.id, userSession).catch(errorCatcher) - revalidatePath("/") - }} - > - - - -
-
-
- ) - }) - : null} + Launch + + + + + ))}
-
- ) : null} + + + }> + {sessions.length ? ( + sessions.map(async (session) => { + const { State } = await docker.getContainer(session.id).inspect(); + const expiresAt = new Date(session.expiresAt); + return ( + +
+ {session.image.friendlyName} + +
+

{session.image.friendlyName}

+

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

+

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

+
+
+ {session.imagePreview ? ( + + + + ) : ( +
+ +
+ )} +
+ {!State.Paused && State.Running ? ( + + + + + + ) : null} + {!State.Paused && State.Running ? ( +
{ + "use server"; + if (!userSession) return; + await manageSession(session.id, "pause", userSession).catch(errorCatcher); + revalidatePath("/"); + }} + > + + + +
+ ) : State.Paused ? ( +
{ + "use server"; + if (!userSession) return; + await manageSession(session.id, "unpause", userSession).catch(errorCatcher); + redirect(`/view/${session.id}?nocheck=true`); + }} + > + + + +
+ ) : null} + {!State.Running ? ( +
{ + "use server"; + if (!userSession) return; + await manageSession(session.id, "start", userSession).catch(errorCatcher); + redirect(`/view/${session.id}?nocheck=true`); + }} + > + + + +
+ ) : State.Running ? ( +
{ + "use server"; + if (!userSession) return; + await manageSession(session.id, "stop", userSession).catch(errorCatcher); + revalidatePath("/"); + }} + > + + + +
+ ) : null} +
{ + "use server"; + if (!userSession) return; + await deleteSession(session.id, userSession).catch(errorCatcher); + revalidatePath("/"); + }} + > + + + +
+
+
+ ); + }) + ) : ( +
+

No sessions found

+

Start a new session to see it here

+
+ )} +
+
+
- ) + ); } diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts index 054b883..faa8b92 100644 --- a/src/app/api/auth/[...nextauth]/route.ts +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -1,5 +1,5 @@ -import { authConfig } from "@/lib/auth" -import NextAuth from "next-auth" +import { authConfig } from "@/lib/auth"; +import NextAuth from "next-auth"; -const handler = NextAuth(authConfig) -export { handler as GET, handler as POST } +const handler = NextAuth(authConfig); +export { handler as GET, handler as POST }; diff --git a/src/app/api/session/files/[slug]/route.ts b/src/app/api/session/files/[slug]/route.ts new file mode 100644 index 0000000..f95a8d2 --- /dev/null +++ b/src/app/api/session/files/[slug]/route.ts @@ -0,0 +1,70 @@ +import { execSync } from "node:child_process"; +import fs from "node:fs"; +import { getAuthSession } from "@/lib/auth"; +import { getSession } from "@/lib/util/get-session"; +import type { NextRequest } from "next/server"; + +export async function GET(_req: NextRequest, { params }: { params: { slug: string } }) { + const userSession = await getAuthSession(); + if (!userSession) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + const { id } = (await getSession(params.slug, userSession)) || {}; + if (!id) { + return Response.json({ error: "Not Found" }, { status: 404 }); + } + execSync(`docker exec ${id} mkdir -p /home/stardust/Downloads`); + const files = execSync(`docker exec ${id} ls /home/stardust/Downloads`).toString().split("\n").filter(Boolean); + return Response.json(files); +} +export async function POST(req: NextRequest, { params }: { params: { slug: string } }) { + const userSession = await getAuthSession(); + const { fileName } = await req.json(); + if (!userSession) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + const { id } = (await getSession(params.slug, userSession)) || {}; + if (!id) { + return Response.json({ error: "Not Found" }, { status: 404 }); + } + try { + const dirPath = `${process.cwd()}/.assets/uploads/${id}`; + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + execSync(`docker cp "${id}:/home/stardust/Downloads/${fileName}" "${dirPath}/${fileName}"`); + const blob = new Blob([fs.readFileSync(`${dirPath}/${fileName}`)]); + fs.unlinkSync(`${dirPath}/${fileName}`); + return new Response(blob, { + headers: { + "Content-Disposition": `attachment; filename=${fileName}`, + "Content-Type": "application/octet-stream", + }, + }); + } catch (e) { + console.error("Download failed: %e", e); + return Response.json({ error: "Download failed" }, { status: 500 }); + } +} +export async function PUT(req: NextRequest, { params }: { params: { slug: string } }) { + const userSession = await getAuthSession(); + const buffer = await req.arrayBuffer(); + const fileName = req.nextUrl.searchParams.get("name"); + if (!userSession) { + return Response.json({ error: "Unauthorized" }, { status: 401 }); + } + const { id } = (await getSession(params.slug, userSession)) || {}; + if (!id) { + return Response.json({ error: "Not Found" }, { status: 404 }); + } + const dirPath = `${process.cwd()}/.assets/uploads/${id}`; + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } + const path = `${dirPath}/${fileName}`; + fs.writeFileSync(path, Buffer.from(buffer)); + execSync(`docker exec ${id} mkdir -p /home/stardust/Uploads`); + execSync(`docker cp "${path}" "${id}:/home/stardust/Uploads/${fileName}"`); + fs.unlinkSync(`${dirPath}/${fileName}`); + return Response.json({ success: true }); +} diff --git a/src/app/api/session/preview/route.ts b/src/app/api/session/preview/route.ts index 1de140b..42ba267 100644 --- a/src/app/api/session/preview/route.ts +++ b/src/app/api/session/preview/route.ts @@ -1,29 +1,28 @@ -import { getAuthSession } from "@/lib/auth" -import { db, session } from "@/lib/drizzle/db" -import { getSession } from "@/lib/util/get-session" -import { eq } from "drizzle-orm" -import { getServerSession } from "next-auth" -import { revalidatePath } from "next/cache" -import type { NextRequest } from "next/server" +import { getAuthSession } from "@/lib/auth"; +import { db, session } from "@/lib/drizzle/db"; +import { getSession } from "@/lib/util/get-session"; +import { eq } from "drizzle-orm"; +import { revalidatePath } from "next/cache"; +import type { NextRequest } from "next/server"; export async function POST(req: NextRequest) { - const { imagePreview, containerId } = await req.json() - const userSession = await getAuthSession() + const { imagePreview, containerId } = await req.json(); + const userSession = await getAuthSession(); if (!userSession) { - return Response.json({ error: "Unauthorized" }, { status: 401 }) + return Response.json({ error: "Unauthorized" }, { status: 401 }); } - const containerSession = await getSession(containerId, userSession) + const containerSession = await getSession(containerId, userSession); if (!containerSession) { - return Response.json({ error: "Not Found" }, { status: 404 }) + return Response.json({ error: "Not Found" }, { status: 404 }); } await db .update(session) .set({ imagePreview }) .where(eq(session.id, containerSession.id)) .catch((e) => { - console.error("Update failed: %e", e) - return Response.json({ error: "Update failed" }, { status: 500 }) - }) - revalidatePath("/") - return Response.json({ success: true }) + console.error("Update failed: %e", e); + return Response.json({ error: "Update failed" }, { status: 500 }); + }); + revalidatePath("/"); + return Response.json({ success: true }); } diff --git a/src/app/api/vnc/[slug]/route.ts b/src/app/api/vnc/[slug]/route.ts index 54b4db9..ba39ce7 100644 --- a/src/app/api/vnc/[slug]/route.ts +++ b/src/app/api/vnc/[slug]/route.ts @@ -1,63 +1,64 @@ -import { getAuthSession } from "@/lib/auth" -import docker from "@/lib/docker" -import { getSession as getContainerSession } from "@/lib/util/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 { getAuthSession } from "@/lib/auth"; +import docker from "@/lib/docker"; +import { getSession as getContainerSession } from "@/lib/util/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"; export async function GET(_req: NextRequest, { params }: { params: { slug: string } }) { - const userSession = await getAuthSession() + const userSession = await getAuthSession(); if (!userSession || !userSession.user) { - return Response.json({ error: "Unauthorized" }, { status: 401 }) + return Response.json({ error: "Unauthorized" }, { status: 401 }); } - const containerSession = await getContainerSession(params.slug, userSession) + const containerSession = await getContainerSession(params.slug, userSession); if (!containerSession) { - return Response.json({ exists: false, error: "Container not found" }, { status: 404 }) + return Response.json({ exists: false, error: "Container not found" }, { status: 404 }); } - const container = docker.getContainer(containerSession.id) - const { State } = await container.inspect() + const container = docker.getContainer(containerSession.id); + const { State } = await container.inspect(); if (!State.Running) { - await container.start() + await container.start(); } - return Response.json({ exists: true, paused: State.Paused }) + return Response.json({ exists: true, paused: State.Paused }); } + export async function SOCKET(ws: WebSocket, req: IncomingMessage, _server: WebSocketServer) { - const containerId = req.url?.split("/")[3] + const containerId = req.url?.split("/")[3]; if (!containerId) { - ws.close() - return + ws.close(); + return; } - const userSession = await getSession({ req }) + const userSession = await getSession({ req }); if (!userSession || !userSession.user) { - ws.close() - return + ws.close(); + return; } - const containerSession = await getContainerSession(containerId, userSession) + const containerSession = await getContainerSession(containerId, userSession); if (!containerSession) { - ws.close() - return + ws.close(); + return; } - const tcpSocket = net.connect(containerSession.vncPort, process.env.CONTAINER_HOST as string) + const tcpSocket = net.connect(containerSession.vncPort, process.env.CONTAINER_HOST as string); ws.on("message", (message: Uint8Array) => { - tcpSocket.write(message) - }) + tcpSocket.write(message); + }); ws.on("close", (code, reason) => { - console.log(`Connection closed due to ${reason} with code ${code}`) - tcpSocket.end() - }) + console.log(`Connection closed due to ${reason} with code ${code}`); + tcpSocket.end(); + }); tcpSocket.on("data", (data) => { - ws.send(data) - }) + ws.send(data); + }); tcpSocket.on("error", (err) => { - console.error(err) - ws.close() - }) + console.error(err); + ws.close(); + }); tcpSocket.on("close", () => { - ws.close() - }) + ws.close(); + }); } diff --git a/src/app/auth/error/page.tsx b/src/app/auth/error/page.tsx index 522fb65..cec3d20 100644 --- a/src/app/auth/error/page.tsx +++ b/src/app/auth/error/page.tsx @@ -1,12 +1,12 @@ -"use client" +"use client"; -import { CardContent, CardHeader } from "@/components/ui/card" -import { ShieldX } from "lucide-react" -import { useSearchParams } from "next/navigation" +import { CardContent, CardHeader } from "@/components/ui/card"; +import { ShieldX } from "lucide-react"; +import { useSearchParams } from "next/navigation"; export default function AuthError() { - const searchParams = useSearchParams() - const error = searchParams.get("error") + const searchParams = useSearchParams(); + const error = searchParams.get("error"); return ( <> @@ -23,5 +23,5 @@ export default function AuthError() {

Please try again. If the problem persists, please contact support.

- ) + ); } diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index 04b6f65..194ff47 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -1,6 +1,6 @@ -import ModeToggle from "@/components/mode-toggle" -import { Card, CardTitle } from "@/components/ui/card" -import { Sparkles } from "lucide-react" +import ModeToggle from "@/components/mode-toggle"; +import { Card, CardTitle } from "@/components/ui/card"; +import { Sparkles } from "lucide-react"; export default function LoginLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( @@ -14,5 +14,5 @@ export default function LoginLayout({ children }: Readonly<{ children: React.Rea - ) + ); } diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 78e3df1..374f90b 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -1,20 +1,20 @@ -"use client" +"use client"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Button } from "@/components/ui/button" -import { CardContent } from "@/components/ui/card" -import { AlertCircle, Loader2, LogIn } from "lucide-react" -import { signIn, useSession } from "next-auth/react" -import { redirect, useSearchParams } from "next/navigation" -import { useState } from "react" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { CardContent } from "@/components/ui/card"; +import { AlertCircle, Loader2, LogIn } from "lucide-react"; +import { signIn, useSession } from "next-auth/react"; +import { redirect, useSearchParams } from "next/navigation"; +import { useState } from "react"; export default function Login() { - const searchParams = useSearchParams() - const [loading, setLoading] = useState(false) - const error = searchParams.get("error") - const { data: session } = useSession() + const searchParams = useSearchParams(); + const [loading, setLoading] = useState(false); + const error = searchParams.get("error"); + const { data: session } = useSession(); if (session) { - redirect("/") + redirect("/"); } return ( @@ -30,8 +30,8 @@ export default function Login() { disabled={loading} className="my-1 w-full" onClick={() => { - setLoading(true) - signIn("auth0") + setLoading(true); + signIn("auth0"); }} > {loading ? : } @@ -39,5 +39,5 @@ export default function Login() { - ) + ); } diff --git a/src/app/auth/logout/page.tsx b/src/app/auth/logout/page.tsx index 76436b0..8d74564 100644 --- a/src/app/auth/logout/page.tsx +++ b/src/app/auth/logout/page.tsx @@ -1,16 +1,16 @@ -"use client" +"use client"; -import { Button } from "@/components/ui/button" -import { CardContent } from "@/components/ui/card" -import { ChevronLeft, Loader2, LogOut } from "lucide-react" -import { signOut } from "next-auth/react" -import { useRouter } from "next/navigation" -import { useState } from "react" -import { toast } from "sonner" +import { Button } from "@/components/ui/button"; +import { CardContent } from "@/components/ui/card"; +import { ChevronLeft, Loader2, LogOut } from "lucide-react"; +import { signOut } from "next-auth/react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { toast } from "sonner"; export default function SignOut() { - const [loading, setLoading] = useState(false) - const router = useRouter() + const [loading, setLoading] = useState(false); + const router = useRouter(); return (

Are you sure you want to log out?

@@ -18,10 +18,10 @@ export default function SignOut() { disabled={loading} className="mt-6 w-full" onClick={async () => { - setLoading(true) - await signOut() - toast("You have been logged out") - router.push("/auth/login") + setLoading(true); + await signOut(); + toast("You have been logged out"); + router.push("/auth/login"); }} > {!loading ? : } @@ -37,5 +37,5 @@ export default function SignOut() { Go back
- ) + ); } diff --git a/src/app/auth/verify/page.tsx b/src/app/auth/verify/page.tsx index 1b4c3aa..439f9f8 100644 --- a/src/app/auth/verify/page.tsx +++ b/src/app/auth/verify/page.tsx @@ -1,12 +1,12 @@ -import { CardContent, CardFooter, CardTitle } from "@/components/ui/card" -import { getAuthSession } from "@/lib/auth" -import { Mail } from "lucide-react" -import { redirect } from "next/navigation" +import { CardContent, CardFooter, CardTitle } from "@/components/ui/card"; +import { getAuthSession } from "@/lib/auth"; +import { Mail } from "lucide-react"; +import { redirect } from "next/navigation"; export default async function EmailVerify() { - const session = await getAuthSession() + const session = await getAuthSession(); if (session) { - redirect("/") + redirect("/"); } return ( <> @@ -26,5 +26,5 @@ export default async function EmailVerify() {

- ) + ); } diff --git a/src/app/globals.css b/src/app/globals.css index 5e03a9e..b8ee6b0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,200 +3,147 @@ @tailwind utilities; @layer base { - :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 10% 3.9%; - - --radius: 0.5rem; - } - - .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; - } - .mocha { - --background: 240 21.053% 14.902%; /* base */ - --foreground: 226.154 63.934% 88.039%; /* text */ - - --muted: 236.842 16.239% 22.941%; /* surface0 */ - --muted-foreground: 226.667 35.294% 80%; /* subtext1 */ - - --popover: 240 21.053% 14.902%; /* base */ - --popover-foreground: 226.154 63.934% 88.039%; /* text */ - - --card: 240 21.053% 14.902%; /* base */ - --card-foreground: 226.154 63.934% 88.039%; /* text */ - - --border: 234.286 13.208% 31.176%; /* surface1 */ - --input: 234.286 13.208% 31.176%; /* surface1 */ - - --primary: 267.407 83.505% 80.98%; /* mauve */ - --primary-foreground: 240 21.053% 14.902%; /* base */ - - --secondary: 236.842 16.239% 22.941%; /* surface0 */ - --secondary-foreground: 226.154 63.934% 88.039%; /* text */ - - --accent: 236.842 16.239% 22.941%; /* surface0 */ - --accent-foreground: 226.154 63.934% 88.039%; /* text */ - - --destructive: 343.269 81.25% 74.902%; /* red */ - --destructive-foreground: 240 21.311% 11.961%; /* mantle */ - - --ring: 226.154 63.934% 88.039%; /* text */ - } - .macchiato { - --background: 231.818 23.404% 18.431%; /* base */ - --foreground: 227.442 68.254% 87.647%; /* text */ - - --muted: 230.4 18.797% 26.078%; /* surface0 */ - --muted-foreground: 228 39.216% 80%; /* subtext1 */ - - --popover: 231.818 23.404% 18.431%; /* base */ - --popover-foreground: 227.442 68.254% 87.647%; /* text */ - - --card: 231.818 23.404% 18.431%; /* base */ - --card-foreground: 227.442 68.254% 87.647%; /* text */ - - --border: 231.111 15.607% 33.922%; /* surface1 */ - --input: 231.111 15.607% 33.922%; /* surface1 */ - - --primary: 266.512 82.692% 79.608%; /* mauve */ - --primary-foreground: 231.818 23.404% 18.431%; /* base */ - - --secondary: 230.4 18.797% 26.078%; /* surface0 */ - --secondary-foreground: 227.442 68.254% 87.647%; /* text */ - - --accent: 230.4 18.797% 26.078%; /* surface0 */ - --accent-foreground: 227.442 68.254% 87.647%; /* text */ - - --destructive: 351.176 73.913% 72.941%; /* red */ - --destructive-foreground: 233.333 23.077% 15.294%; /* mantle */ - - --ring: 227.442 68.254% 87.647%; /* text */ - - --radius: 0.5rem; - } - .frappe { - --background: 229.091 18.644% 23.137%; /* base */ - --foreground: 227.234 70.149% 86.863%; /* text */ - - --muted: 230 15.584% 30.196%; /* surface0 */ - --muted-foreground: 226.667 43.689% 79.804%; /* subtext1 */ - - --popover: 229.091 18.644% 23.137%; /* base */ - --popover-foreground: 227.234 70.149% 86.863%; /* text */ - - --card: 229.091 18.644% 23.137%; /* base */ - --card-foreground: 227.234 70.149% 86.863%; /* text */ - - --border: 227.143 14.737% 37.255%; /* surface1 */ - --input: 227.143 14.737% 37.255%; /* surface1 */ - - --primary: 276.667 59.016% 76.078%; /* mauve */ - --primary-foreground: 229.091 18.644% 23.137%; /* base */ - - --secondary: 230 15.584% 30.196%; /* surface0 */ - --secondary-foreground: 227.234 70.149% 86.863%; /* text */ - - --accent: 230 15.584% 30.196%; /* surface0 */ - --accent-foreground: 227.234 70.149% 86.863%; /* text */ - - --destructive: 358.812 67.785% 70.784%; /* red */ - --destructive-foreground: 230.526 18.812% 19.804%; /* mantle */ - - --ring: 227.234 70.149% 86.863%; /* text */ - - --radius: 0.5rem; - } - .latte { - --background: 220 23.077% 94.902%; /* base */ - --foreground: 233.793 16.022% 35.49%; /* text */ - - --muted: 222.857 15.909% 82.745%; /* surface0 */ - --muted-foreground: 233.333 12.796% 41.373%; /* subtext1 */ - - --popover: 220 23.077% 94.902%; /* base */ - --popover-foreground: 233.793 16.022% 35.49%; /* text */ - - --card: 220 23.077% 94.902%; /* base */ - --card-foreground: 233.793 16.022% 35.49%; /* text */ - - --border: 225 13.559% 76.863%; /* surface1 */ - --input: 225 13.559% 76.863%; /* surface1 */ - - --primary: 266.044 85.047% 58.039%; /* mauve */ - --primary-foreground: 220 23.077% 94.902%; /* base */ - - --secondary: 222.857 15.909% 82.745%; /* surface0 */ - --secondary-foreground: 233.793 16.022% 35.49%; /* text */ - - --accent: 222.857 15.909% 82.745%; /* surface0 */ - --accent-foreground: 233.793 16.022% 35.49%; /* text */ - - --destructive: 347.077 86.667% 44.118%; /* red */ - --destructive-foreground: 220 21.951% 91.961%; /* mantle */ - - --ring: 233.793 16.022% 35.49%; /* text */ - - --radius: 0.5rem; - } + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + } + + .blue { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 217.2 91.2% 59.8%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 224.3 76.3% 48%; + } + + .mocha { + --background: 240 21.053% 14.902%; + /* base */ + --foreground: 226.154 63.934% 88.039%; + /* text */ + + --muted: 236.842 16.239% 22.941%; + /* surface0 */ + --muted-foreground: 226.667 35.294% 80%; + /* subtext1 */ + + --popover: 240 21.053% 14.902%; + /* base */ + --popover-foreground: 226.154 63.934% 88.039%; + /* text */ + + --card: 240 21.053% 14.902%; + /* base */ + --card-foreground: 226.154 63.934% 88.039%; + /* text */ + + --border: 234.286 13.208% 31.176%; + /* surface1 */ + --input: 234.286 13.208% 31.176%; + /* surface1 */ + + --primary: 267.407 83.505% 80.98%; + /* mauve */ + --primary-foreground: 240 21.053% 14.902%; + /* base */ + + --secondary: 236.842 16.239% 22.941%; + /* surface0 */ + --secondary-foreground: 226.154 63.934% 88.039%; + /* text */ + + --accent: 236.842 16.239% 22.941%; + /* surface0 */ + --accent-foreground: 226.154 63.934% 88.039%; + /* text */ + + --destructive: 343.269 81.25% 74.902%; + /* red */ + --destructive-foreground: 240 21.311% 11.961%; + /* mantle */ + + --ring: 226.154 63.934% 88.039%; + /* text */ + } } @layer base { - * { - @apply border-border; - } - body { - @apply bg-background text-foreground; - } + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index de3d847..e362efd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,12 +1,13 @@ -import { Toaster } from "@/components/ui/sonner" -import { getAuthSession } from "@/lib/auth" -import { readFileSync } from "node:fs" -import type { Metadata } from "next" -import { ThemeProvider } from "next-themes" -import { Inter } from "next/font/google" -import "./globals.css" -import { Session } from "./providers" -const inter = Inter({ subsets: ["latin"] }) +import { readFileSync } from "node:fs"; +import { Toaster } from "@/components/ui/sonner"; +import { getAuthSession } from "@/lib/auth"; +import type { Metadata } from "next"; +import { ThemeProvider } from "next-themes"; +import { Inter, JetBrains_Mono } from "next/font/google"; +import "./globals.css"; +import { Session } from "./providers"; +const inter = Inter({ subsets: ["latin"] }); +const jetbrains = JetBrains_Mono({ subsets: ["latin"], variable: "--mono" }); export const metadata: Metadata = { title: "Stardust", @@ -17,17 +18,17 @@ export const metadata: Metadata = { type: "website", url: "https://stardust.spaceness.one", // probably going to be a personal website for stardust, }, -} +}; export default async function RootLayout({ children, }: Readonly<{ - children: React.ReactNode + children: React.ReactNode; }>) { - const config = JSON.parse(readFileSync(`${process.cwd()}/config.json`, "utf-8")) + const config = JSON.parse(readFileSync(`${process.cwd()}/config.json`, "utf-8")); return ( - + {children} - ) + ); } diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index d8a0d5c..b24bc70 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,6 +1,6 @@ -import { Button } from "@/components/ui/button" -import { Sparkles } from "lucide-react" -import Link from "next/link" +import { Button } from "@/components/ui/button"; +import { Sparkles } from "lucide-react"; +import Link from "next/link"; export default function NotFound() { return ( @@ -19,5 +19,5 @@ export default function NotFound() { - ) + ); } diff --git a/src/app/providers.tsx b/src/app/providers.tsx index cb45203..35b3530 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -1,5 +1,5 @@ -"use client" +"use client"; -import { SessionProvider, type SessionProviderProps } from "next-auth/react" +import { SessionProvider, type SessionProviderProps } from "next-auth/react"; -export const Session = ({ ...props }: SessionProviderProps) => +export const Session = ({ ...props }: SessionProviderProps) => ; diff --git a/src/app/view/[slug]/page.tsx b/src/app/view/[slug]/page.tsx index 0270721..dedd205 100644 --- a/src/app/view/[slug]/page.tsx +++ b/src/app/view/[slug]/page.tsx @@ -1,6 +1,7 @@ -"use client" +"use client"; -import { deleteSession, manageSession } from "@/actions/client-session" +import { deleteSession, manageSession } from "@/actions/client-session"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { AlertDialog, AlertDialogAction, @@ -11,22 +12,29 @@ import { AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, -} from "@/components/ui/alert-dialog" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +} from "@/components/ui/alert-dialog"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Button } from "@/components/ui/button" -import { Label } from "@/components/ui/label" -import { Separator } from "@/components/ui/separator" +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; -import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet" -import { Slider } from "@/components/ui/slider" -import { Switch } from "@/components/ui/switch" -import { Textarea } from "@/components/ui/textarea" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import type { VncViewerHandle } from "@/components/vnc-screen" +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Slider } from "@/components/ui/slider"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import type { VncViewerHandle } from "@/components/vnc-screen"; +import { fetcher } from "@/lib/utils"; import { Camera, ChevronRight, + Download, + File, + Info, LogOut, LucideHome, Maximize, @@ -35,106 +43,123 @@ import { RotateCw, Sparkles, TrashIcon, -} from "lucide-react" -import Link from "next/link" -import { notFound, useSearchParams } from "next/navigation" -import { Suspense, lazy, useEffect, useRef, useState } from "react" -import { toast } from "sonner" -type ScalingValues = "remote" | "local" | "none" +} from "lucide-react"; +import Link from "next/link"; +import { notFound, useSearchParams } from "next/navigation"; +import { Suspense, lazy, useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; +import useSWR, { useSWRConfig } from "swr"; +type ScalingValues = "remote" | "local" | "none"; const Loading = ({ text }: { text: string }) => ( -
- +
+

{text}

-) -const VncScreen = lazy(() => import("@/components/vnc-screen")) +); +const VncScreen = lazy(() => import("@/components/vnc-screen")); export default function View({ params }: { params: { slug: string } }) { - const vncRef = useRef(null) + const vncRef = useRef(null); const [session, setSession] = useState<{ - exists: boolean - url: string | null - paused?: boolean - } | null>(null) - const [connected, setConnected] = useState(false) - const [pausing, setPausing] = useState(false) - const [fullScreen, setFullScreen] = useState(false) - const [sidebarOpen, setSidebarOpen] = useState(false) - const [workingClipboard, setWorkingClipboard] = useState(true) + exists: boolean; + url: string | null; + paused?: boolean; + } | null>(null); + const [connected, setConnected] = useState(false); + const [fullScreen, setFullScreen] = useState(false); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [workingClipboard, setWorkingClipboard] = useState(true); // noVNC options start - const [clipboard, setClipboard] = useState("") - const [viewOnly, setViewOnly] = useState(false) - const [qualityLevel, setQualityLevel] = useState(6) - const [compressionLevel, setCompressionLevel] = useState(2) - const [clipViewport, setClipViewport] = useState(false) - const [scaling, setScaling] = useState("remote") + const [clipboard, setClipboard] = useState(""); + const [viewOnly, setViewOnly] = useState(false); + const [qualityLevel, setQualityLevel] = useState(6); + const [compressionLevel, setCompressionLevel] = useState(2); + const [clipViewport, setClipViewport] = useState(false); + const [scaling, setScaling] = useState("remote"); // noVNC options end - const searchParams = useSearchParams() + const searchParams = useSearchParams(); + const { mutate } = useSWRConfig(); + const { + data: filesList, + error: filesError, + isLoading: filesLoading, + } = useSWR(`/api/session/files/${params.slug}`, fetcher, { refreshInterval: 10000 }); useEffect(() => { try { if (searchParams.get("nocheck") === "true") { - setSession({ exists: true, url: `/api/vnc/${params.slug}` }) + setSession({ exists: true, url: `/api/vnc/${params.slug}` }); } else { fetch(`/api/vnc/${params.slug}`) .then((res) => res.json()) .then((data) => { if (data.exists) { - setSession({ exists: true, url: `/api/vnc/${params.slug}`, paused: data.paused ?? false }) + setSession({ + exists: true, + url: `/api/vnc/${params.slug}`, + paused: data.paused ?? false, + }); } else { - setSession({ exists: false, url: null }) + setSession({ exists: false, url: null }); } - }) + }); } } catch (error) { - throw new Error(error as string) + throw new Error(error as string); } - }, [params.slug, searchParams]) + }, [params.slug, searchParams]); useEffect(() => { if (connected && vncRef.current?.rfb) { - vncRef.current.rfb.viewOnly = viewOnly - vncRef.current.rfb.qualityLevel = qualityLevel - vncRef.current.rfb.compressionLevel = compressionLevel - vncRef.current.rfb.clipViewport = clipViewport - vncRef.current.rfb.resizeSession = scaling === "remote" - vncRef.current.rfb.scaleViewport = scaling === "local" + vncRef.current.rfb.viewOnly = viewOnly; + vncRef.current.rfb.qualityLevel = qualityLevel; + vncRef.current.rfb.compressionLevel = compressionLevel; + vncRef.current.rfb.clipViewport = clipViewport; + vncRef.current.rfb.resizeSession = scaling === "remote"; + vncRef.current.rfb.scaleViewport = scaling === "local"; } - }, [connected, viewOnly, qualityLevel, compressionLevel, scaling, clipViewport]) + }, [connected, viewOnly, qualityLevel, compressionLevel, scaling, clipViewport]); // jank, but it works ¯\_(ツ)_/¯ useEffect(() => { const interval = setInterval(() => { if (connected && vncRef.current?.rfb && document.hasFocus()) { - vncRef.current.rfb.focus() + vncRef.current.rfb.focus(); navigator.clipboard .readText() .then((text) => { if (text !== clipboard) { - setWorkingClipboard(true) - vncRef.current?.clipboardPaste(text) - setClipboard(text) + setWorkingClipboard(true); + vncRef.current?.clipboardPaste(text); + setClipboard(text); } }) - .catch(() => setWorkingClipboard(false)) + .catch(() => setWorkingClipboard(false)); } - }, 250) - return () => clearInterval(interval) - }, [connected, clipboard]) + }, 250); + return () => clearInterval(interval); + }, [connected, clipboard]); useEffect(() => { const interval = setInterval(() => { if (connected && vncRef.current?.rfb) { fetch("/api/session/preview", { method: "POST", - body: JSON.stringify({ imagePreview: vncRef.current.rfb?.toDataURL(), containerId: params.slug }), - }) + body: JSON.stringify({ + imagePreview: vncRef.current.rfb?.toDataURL(undefined, 0.25), + containerId: params.slug, + }), + }); } - }, 10000) - return () => clearInterval(interval) - }, [connected, params.slug]) + }, 10000); + return () => clearInterval(interval); + }, [connected, params.slug]); useEffect(() => { if (document.fullscreenElement === null && fullScreen) { - document.documentElement.requestFullscreen() + document.documentElement.requestFullscreen(); } else if (document.fullscreenElement !== null && !fullScreen) { - document.exitFullscreen() + document.exitFullscreen(); } - }, [fullScreen]) + }, [fullScreen]); + // biome-ignore lint: + useEffect(() => { + if (sidebarOpen) mutate(`/api/session/files/${params.slug}`); + }, [sidebarOpen]); return (
{connected ? ( @@ -149,27 +174,135 @@ export default function View({ params }: { params: { slug: string } }) { ) : null} - + Settings Clipboard - Contents of the clipboard will be shared with the remote machine. + Contents of the clipboard will be shared with the remote machine.