From 47127f276d4f71c8f9cca3177e6e0fe9ff72a1c0 Mon Sep 17 00:00:00 2001 From: kawa Date: Sun, 8 Mar 2026 15:49:34 +0100 Subject: [PATCH] Initial push --- app/(auth)/accept-invite/page.tsx | 166 ++++ app/(auth)/login/page.tsx | 377 +++++++++ app/(dashboard)/audit/page.tsx | 198 +++++ app/(dashboard)/backups/page.tsx | 324 +++++++ app/(dashboard)/console/page.tsx | 254 ++++++ app/(dashboard)/files/page.tsx | 441 ++++++++++ app/(dashboard)/layout.tsx | 31 + app/(dashboard)/map/page.tsx | 127 +++ app/(dashboard)/monitoring/page.tsx | 269 ++++++ app/(dashboard)/page.tsx | 277 ++++++ app/(dashboard)/players/page.tsx | 386 +++++++++ app/(dashboard)/plugins/page.tsx | 275 ++++++ app/(dashboard)/scheduler/page.tsx | 346 ++++++++ app/(dashboard)/server/page.tsx | 362 ++++++++ app/(dashboard)/team/page.tsx | 239 ++++++ app/api/accept-invite/route.ts | 71 ++ app/api/audit/route.ts | 48 ++ app/api/backups/[id]/route.ts | 56 ++ app/api/backups/route.ts | 48 ++ app/api/files/delete/route.ts | 47 ++ app/api/files/download/route.ts | 37 + app/api/files/list/route.ts | 60 ++ app/api/files/upload/route.ts | 71 ++ app/api/health/route.ts | 9 + app/api/monitoring/route.ts | 66 ++ app/api/players/[id]/route.ts | 133 +++ app/api/players/route.ts | 42 + app/api/plugins/route.ts | 88 ++ app/api/scheduler/[id]/route.ts | 69 ++ app/api/scheduler/route.ts | 67 ++ app/api/server/control/route.ts | 74 ++ app/api/server/settings/route.ts | 66 ++ app/api/server/status/route.ts | 20 + app/api/server/versions/route.ts | 35 + app/api/team/route.ts | 91 ++ app/layout.tsx | 59 +- bun.lock | 69 ++ components/layout/providers.tsx | 72 ++ components/layout/sidebar.tsx | 481 +++++++++++ components/layout/topbar.tsx | 458 ++++++++++ components/ui/accordion.tsx | 74 ++ components/ui/alert-dialog.tsx | 187 ++++ components/ui/alert.tsx | 76 ++ components/ui/avatar.tsx | 109 +++ components/ui/badge.tsx | 52 ++ components/ui/card.tsx | 103 +++ components/ui/checkbox.tsx | 29 + components/ui/command.tsx | 196 +++++ components/ui/context-menu.tsx | 271 ++++++ components/ui/dialog.tsx | 157 ++++ components/ui/dropdown-menu.tsx | 271 ++++++ components/ui/hover-card.tsx | 51 ++ components/ui/input-group.tsx | 158 ++++ components/ui/input.tsx | 20 + components/ui/label.tsx | 20 + components/ui/menubar.tsx | 283 +++++++ components/ui/navigation-menu.tsx | 168 ++++ components/ui/popover.tsx | 90 ++ components/ui/progress.tsx | 83 ++ components/ui/radio-group.tsx | 38 + components/ui/scroll-area.tsx | 55 ++ components/ui/select.tsx | 201 +++++ components/ui/separator.tsx | 25 + components/ui/sheet.tsx | 135 +++ components/ui/skeleton.tsx | 13 + components/ui/switch.tsx | 32 + components/ui/table.tsx | 116 +++ components/ui/tabs.tsx | 82 ++ components/ui/textarea.tsx | 18 + components/ui/tooltip.tsx | 66 ++ data/cubeadmin.db | Bin 0 -> 4096 bytes data/cubeadmin.db-shm | Bin 0 -> 32768 bytes data/cubeadmin.db-wal | 0 docker/Dockerfile | 45 + docker/docker-compose.dev.yml | 36 + docker/docker-compose.yml | 129 +++ drizzle.config.ts | 12 + drizzle/0000_overjoyed_thundra.sql | 180 ++++ drizzle/meta/0000_snapshot.json | 1222 +++++++++++++++++++++++++++ drizzle/meta/_journal.json | 13 + lib/auth/client.ts | 48 ++ lib/auth/index.ts | 108 +++ lib/backup/manager.ts | 141 ++++ lib/db/index.ts | 102 +++ lib/db/migrate.ts | 31 + lib/db/schema.ts | 359 ++++++++ lib/email/index.ts | 53 ++ lib/email/templates/invitation.tsx | 78 ++ lib/minecraft/process.ts | 269 ++++++ lib/minecraft/rcon.ts | 129 +++ lib/minecraft/sync.ts | 104 +++ lib/minecraft/versions.ts | 324 +++++++ lib/scheduler/index.ts | 75 ++ lib/security/rateLimit.ts | 59 ++ lib/security/sanitize.ts | 60 ++ lib/socket/server.ts | 258 ++++++ next.config.ts | 123 ++- package.json | 3 + proxy.ts | 201 +++++ server.ts | 99 +++ tsconfig.json | 3 +- 101 files changed, 13844 insertions(+), 8 deletions(-) create mode 100644 app/(auth)/accept-invite/page.tsx create mode 100644 app/(auth)/login/page.tsx create mode 100644 app/(dashboard)/audit/page.tsx create mode 100644 app/(dashboard)/backups/page.tsx create mode 100644 app/(dashboard)/console/page.tsx create mode 100644 app/(dashboard)/files/page.tsx create mode 100644 app/(dashboard)/layout.tsx create mode 100644 app/(dashboard)/map/page.tsx create mode 100644 app/(dashboard)/monitoring/page.tsx create mode 100644 app/(dashboard)/page.tsx create mode 100644 app/(dashboard)/players/page.tsx create mode 100644 app/(dashboard)/plugins/page.tsx create mode 100644 app/(dashboard)/scheduler/page.tsx create mode 100644 app/(dashboard)/server/page.tsx create mode 100644 app/(dashboard)/team/page.tsx create mode 100644 app/api/accept-invite/route.ts create mode 100644 app/api/audit/route.ts create mode 100644 app/api/backups/[id]/route.ts create mode 100644 app/api/backups/route.ts create mode 100644 app/api/files/delete/route.ts create mode 100644 app/api/files/download/route.ts create mode 100644 app/api/files/list/route.ts create mode 100644 app/api/files/upload/route.ts create mode 100644 app/api/health/route.ts create mode 100644 app/api/monitoring/route.ts create mode 100644 app/api/players/[id]/route.ts create mode 100644 app/api/players/route.ts create mode 100644 app/api/plugins/route.ts create mode 100644 app/api/scheduler/[id]/route.ts create mode 100644 app/api/scheduler/route.ts create mode 100644 app/api/server/control/route.ts create mode 100644 app/api/server/settings/route.ts create mode 100644 app/api/server/status/route.ts create mode 100644 app/api/server/versions/route.ts create mode 100644 app/api/team/route.ts create mode 100644 components/layout/providers.tsx create mode 100644 components/layout/sidebar.tsx create mode 100644 components/layout/topbar.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-group.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 data/cubeadmin.db create mode 100644 data/cubeadmin.db-shm create mode 100644 data/cubeadmin.db-wal create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.dev.yml create mode 100644 docker/docker-compose.yml create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_overjoyed_thundra.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 lib/auth/client.ts create mode 100644 lib/auth/index.ts create mode 100644 lib/backup/manager.ts create mode 100644 lib/db/index.ts create mode 100644 lib/db/migrate.ts create mode 100644 lib/db/schema.ts create mode 100644 lib/email/index.ts create mode 100644 lib/email/templates/invitation.tsx create mode 100644 lib/minecraft/process.ts create mode 100644 lib/minecraft/rcon.ts create mode 100644 lib/minecraft/sync.ts create mode 100644 lib/minecraft/versions.ts create mode 100644 lib/scheduler/index.ts create mode 100644 lib/security/rateLimit.ts create mode 100644 lib/security/sanitize.ts create mode 100644 lib/socket/server.ts create mode 100644 proxy.ts create mode 100644 server.ts diff --git a/app/(auth)/accept-invite/page.tsx b/app/(auth)/accept-invite/page.tsx new file mode 100644 index 0000000..8e0ffe8 --- /dev/null +++ b/app/(auth)/accept-invite/page.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { Suspense, useState, useEffect } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Alert } from "@/components/ui/alert"; +import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react"; + +function AcceptInvitePageInner() { + const router = useRouter(); + const searchParams = useSearchParams(); + const token = searchParams.get("token"); + + const [name, setName] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + useEffect(() => { + if (!token) { + setError("Invalid or missing invitation token."); + } + }, [token]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (password !== confirmPassword) { + setError("Passwords do not match"); + return; + } + if (password.length < 8) { + setError("Password must be at least 8 characters"); + return; + } + + setLoading(true); + setError(null); + + try { + const res = await fetch("/api/accept-invite", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token, name, password }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error); + setSuccess(true); + setTimeout(() => router.push("/login"), 2000); + } catch (err) { + setError(err instanceof Error ? err.message : "Something went wrong"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {/* Logo */} +
+
+ + + + + + + CubeAdmin +
+
+ + + + Accept Invitation + + Create your account to access the Minecraft server panel. + + + + {success ? ( +
+ +

Account created!

+

+ Redirecting you to the login page... +

+
+ ) : ( +
+ {error && ( + + + {error} + + )} +
+ + setName(e.target.value)} + placeholder="John Doe" + required + disabled={!token || loading} + className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 focus:border-emerald-500/50" + /> +
+
+ + setPassword(e.target.value)} + placeholder="At least 8 characters" + required + disabled={!token || loading} + className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 focus:border-emerald-500/50" + /> +
+
+ + setConfirmPassword(e.target.value)} + placeholder="Repeat your password" + required + disabled={!token || loading} + className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500 focus:border-emerald-500/50" + /> +
+ +
+ )} +
+
+
+
+ ); +} + +export default function AcceptInvitePage() { + return ( + + + + ); +} diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx new file mode 100644 index 0000000..7c65fd5 --- /dev/null +++ b/app/(auth)/login/page.tsx @@ -0,0 +1,377 @@ +"use client"; + +import React, { Suspense, useState, useTransition } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { z } from "zod"; +import { toast } from "sonner"; +import { authClient } from "@/lib/auth/client"; +import { cn } from "@/lib/utils"; +import { Eye, EyeOff, Loader2, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +// --------------------------------------------------------------------------- +// Validation schema +// --------------------------------------------------------------------------- +const loginSchema = z.object({ + email: z.string().email("Please enter a valid email address"), + password: z.string().min(1, "Password is required"), +}); + +type LoginFormValues = z.infer; +type FieldErrors = Partial>; + +// --------------------------------------------------------------------------- +// CubeAdmin logo (duplicated from sidebar so this page is self-contained) +// --------------------------------------------------------------------------- +function CubeIcon({ className }: { className?: string }) { + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Form field component +// --------------------------------------------------------------------------- +interface FormFieldProps { + id: string; + label: string; + type?: string; + value: string; + onChange: (value: string) => void; + error?: string; + placeholder?: string; + autoComplete?: string; + disabled?: boolean; + children?: React.ReactNode; // for additional elements (e.g., show/hide button) + className?: string; +} + +function FormField({ + id, + label, + type = "text", + value, + onChange, + error, + placeholder, + autoComplete, + disabled, + children, + className, +}: FormFieldProps) { + return ( +
+ +
+ onChange((e.target as HTMLInputElement).value)} + placeholder={placeholder} + autoComplete={autoComplete} + disabled={disabled} + aria-invalid={!!error} + aria-describedby={error ? `${id}-error` : undefined} + className={cn( + "h-9 bg-zinc-900/60 border-zinc-800 text-zinc-100 placeholder:text-zinc-600 focus-visible:border-emerald-500/50 focus-visible:ring-emerald-500/20", + children && "pr-10", + error && + "border-red-500/50 focus-visible:border-red-500/50 focus-visible:ring-red-500/20" + )} + /> + {children} +
+ {error && ( + + )} +
+ ); +} + +// --------------------------------------------------------------------------- +// Login page +// --------------------------------------------------------------------------- +function LoginPageInner() { + const router = useRouter(); + const searchParams = useSearchParams(); + const callbackUrl = searchParams.get("callbackUrl") ?? "/dashboard"; + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [fieldErrors, setFieldErrors] = useState({}); + const [globalError, setGlobalError] = useState(null); + const [isPending, startTransition] = useTransition(); + + function validate(): LoginFormValues | null { + const result = loginSchema.safeParse({ email, password }); + if (!result.success) { + const errors: FieldErrors = {}; + for (const issue of result.error.issues) { + const field = issue.path[0] as keyof LoginFormValues; + if (!errors[field]) errors[field] = issue.message; + } + setFieldErrors(errors); + return null; + } + setFieldErrors({}); + return result.data; + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setGlobalError(null); + + const values = validate(); + if (!values) return; + + startTransition(async () => { + try { + const { error } = await authClient.signIn.email({ + email: values.email, + password: values.password, + }); + + if (error) { + // Map common Better Auth error codes to friendly messages + const message = mapAuthError(error.code ?? error.message ?? ""); + setGlobalError(message); + return; + } + + toast.success("Signed in successfully"); + router.push(callbackUrl); + router.refresh(); + } catch (err) { + setGlobalError("An unexpected error occurred. Please try again."); + console.error("[login] Unexpected error:", err); + } + }); + } + + return ( +
+ {/* Background grid pattern */} +