From c8895c8e80843840a777d2c126f01d87321cf5a1 Mon Sep 17 00:00:00 2001 From: kawa Date: Sun, 8 Mar 2026 17:01:36 +0100 Subject: [PATCH] BugFixes galore --- app/(auth)/login/page.tsx | 13 +- app/(auth)/register/page.tsx | 321 ++++++ app/(dashboard)/console/page.tsx | 5 +- app/(dashboard)/{ => dashboard}/page.tsx | 0 app/(dashboard)/settings/page.tsx | 183 ++++ app/(dashboard)/updates/page.tsx | 254 +++++ app/api/audit/route.ts | 4 +- app/api/auth/[...all]/route.ts | 4 + app/api/backups/[id]/route.ts | 6 +- app/api/backups/route.ts | 6 +- app/api/files/delete/route.ts | 4 +- app/api/files/download/route.ts | 4 +- app/api/files/list/route.ts | 4 +- app/api/files/upload/route.ts | 4 +- app/api/monitoring/route.ts | 4 +- app/api/players/[id]/route.ts | 6 +- app/api/players/route.ts | 4 +- app/api/plugins/route.ts | 6 +- app/api/scheduler/[id]/route.ts | 6 +- app/api/scheduler/route.ts | 6 +- app/api/server/control/route.ts | 4 +- app/api/server/settings/route.ts | 6 +- app/api/server/status/route.ts | 4 +- app/api/server/versions/route.ts | 4 +- app/api/team/route.ts | 6 +- app/page.tsx | 66 +- components/layout/sidebar.tsx | 49 +- components/layout/topbar.tsx | 72 +- data/cubeadmin.db-shm | Bin 32768 -> 32768 bytes data/cubeadmin.db-wal | Bin 0 -> 284312 bytes drizzle/0001_gifted_loa.sql | 2 + drizzle/meta/0001_snapshot.json | 1236 ++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + lib/auth/client.ts | 6 +- lib/auth/index.ts | 53 +- lib/db/schema.ts | 24 +- next.config.ts | 84 +- proxy.ts | 14 +- types/better-auth.d.ts | 11 + 39 files changed, 2255 insertions(+), 237 deletions(-) create mode 100644 app/(auth)/register/page.tsx rename app/(dashboard)/{ => dashboard}/page.tsx (100%) create mode 100644 app/(dashboard)/settings/page.tsx create mode 100644 app/(dashboard)/updates/page.tsx create mode 100644 app/api/auth/[...all]/route.ts create mode 100644 drizzle/0001_gifted_loa.sql create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 types/better-auth.d.ts diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 7c65fd5..e39e73d 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -315,8 +315,19 @@ function LoginPageInner() { + {/* Register link */} +

+ No account?{" "} + + Create one + +

+ {/* Footer */} -

+

CubeAdmin — Secure server management

diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx new file mode 100644 index 0000000..cdd044e --- /dev/null +++ b/app/(auth)/register/page.tsx @@ -0,0 +1,321 @@ +"use client"; + +import React, { useState, useTransition } from "react"; +import { useRouter } 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"; + +const registerSchema = z + .object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.string().email("Please enter a valid email address"), + password: z.string().min(8, "Password must be at least 8 characters"), + confirm: z.string(), + }) + .refine((d) => d.password === d.confirm, { + message: "Passwords do not match", + path: ["confirm"], + }); + +type RegisterFormValues = z.infer; +type FieldErrors = Partial>; + +function CubeIcon({ className }: { className?: string }) { + return ( + + ); +} + +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; +} + +function FormField({ + id, + label, + type = "text", + value, + onChange, + error, + placeholder, + autoComplete, + disabled, + children, +}: 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 && ( + + )} +
+ ); +} + +export default function RegisterPage() { + const router = useRouter(); + + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + const [fieldErrors, setFieldErrors] = useState({}); + const [globalError, setGlobalError] = useState(null); + const [isPending, startTransition] = useTransition(); + + function validate(): RegisterFormValues | null { + const result = registerSchema.safeParse({ name, email, password, confirm }); + if (!result.success) { + const errors: FieldErrors = {}; + for (const issue of result.error.issues) { + const field = issue.path[0] as keyof RegisterFormValues; + 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.signUp.email({ + name: values.name, + email: values.email, + password: values.password, + }); + + if (error) { + const msg = error.code?.toLowerCase().includes("user_already_exists") + ? "An account with this email already exists." + : (error.message ?? "Registration failed. Please try again."); + setGlobalError(msg); + return; + } + + toast.success("Account created — welcome to CubeAdmin!"); + router.push("/dashboard"); + router.refresh(); + } catch { + setGlobalError("An unexpected error occurred. Please try again."); + } + }); + } + + return ( +
+ {/* Background grid */} + diff --git a/components/layout/topbar.tsx b/components/layout/topbar.tsx index 1549b39..d3b39ff 100644 --- a/components/layout/topbar.tsx +++ b/components/layout/topbar.tsx @@ -22,6 +22,7 @@ import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, + DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, @@ -54,7 +55,8 @@ const PAGE_TITLES: Record = { "/plugins": "Plugins", "/files": "File Manager", "/backups": "Backups", - "/settings": "Server Settings", + "/settings": "Account Settings", + "/server": "Server Settings", "/updates": "Updates", "/team": "Team", "/audit": "Audit Log", @@ -96,7 +98,7 @@ function ServerStatusBadge({ status }: { status: ServerStatus | undefined }) { ); } - const config = { + const statusConfigs = { online: { dot: "bg-emerald-500", text: "Online", @@ -117,7 +119,12 @@ function ServerStatusBadge({ status }: { status: ServerStatus | undefined }) { text: "Stopping…", className: "border-orange-500/20 bg-orange-500/10 text-orange-400", }, - }[status.status]; + }; + const config = statusConfigs[status.status] ?? { + dot: "bg-zinc-500", + text: status.status, + className: "border-zinc-700/50 bg-zinc-800/50 text-zinc-400", + }; return ( { + setMounted(true); + }, []); + + // Render a placeholder until mounted to avoid SSR/client mismatch + if (!mounted) { + return ( + + ); + } + const isDark = resolvedTheme === "dark"; return ( @@ -344,24 +372,30 @@ function UserMenu() { - -
- {session?.user?.name ?? "—"} - - {session?.user?.email ?? "—"} - -
-
+ + +
+ {session?.user?.name ?? "—"} + + {session?.user?.email ?? "—"} + +
+
+
- router.push("/settings")}> - - Settings - + + router.push("/settings")}> + + Settings + + - - - Sign out - + + + + Sign out + +
); diff --git a/data/cubeadmin.db-shm b/data/cubeadmin.db-shm index fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10..5d4e46cf3c6e028fd4cda6f82c1547436ea84d53 100644 GIT binary patch literal 32768 zcmeI*$xc*J6ouhcGy--Y2o^R_K#8I>_6pJ{%8QUVal$(=_N^Qdn_h|BK@A}T?58L}6PmbHWORJxL z93Jd_|I+-&^ZuxxiO)Ul$gj75Klr=V<#xNB&wKp3*S+BOx&7{dd(pk*UUsjzSKXLl z$i3zcyCd#(chntoZ@5i&+`Z}Ea&NnL+zEHmZMjqKv^(R5MawPTi{}kDKCgI;_lmz4 zzqj);vALC#c0Aa>f26oGE<6k1L>$eGFr#5O0ABG9L& zT1z>|naT*n?l6)f(647&PdUh$$_T^;KawI4`x8lC0J82OdG7o&s_EBSD~^0&(vtL7<)jaYHXbpq>J8*EK<)o&s^pJ4q2Z E0}sDC%>V!Z delta 84 zcmZo@U}|V!;+1%$%K!t6lNT~di(0Ttu$fN2&50o+0+WHM{|^Kcb7VGeU{rAc5+U{g D(Pk78 diff --git a/data/cubeadmin.db-wal b/data/cubeadmin.db-wal index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9497e224ee7b1ea715962096b9572497cfe1e048 100644 GIT binary patch literal 284312 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 statement-breakpoint +ALTER TABLE `accounts` ADD `password` text; \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..f0fd607 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1236 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6bd4ab07-5bb3-41eb-8a1a-cd6a392e1152", + "prevId": "6c037435-c4bf-4871-912d-11eb618c4e68", + "tables": { + "accounts": { + "name": "accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_logs": { + "name": "audit_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "details": { + "name": "details", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "backups": { + "name": "backups", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "triggered_by": { + "name": "triggered_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "backups_triggered_by_users_id_fk": { + "name": "backups_triggered_by_users_id_fk", + "tableFrom": "backups", + "tableTo": "users", + "columnsFrom": [ + "triggered_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitations": { + "name": "invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'moderator'" + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitations_token_unique": { + "name": "invitations_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invitations_invited_by_users_id_fk": { + "name": "invitations_invited_by_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mc_players": { + "name": "mc_players", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "first_seen": { + "name": "first_seen", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_seen": { + "name": "last_seen", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_online": { + "name": "is_online", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "play_time": { + "name": "play_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_banned": { + "name": "is_banned", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "mc_players_uuid_idx": { + "name": "mc_players_uuid_idx", + "columns": [ + "uuid" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "player_bans": { + "name": "player_bans", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned_by": { + "name": "banned_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned_at": { + "name": "banned_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "unbanned_by": { + "name": "unbanned_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unbanned_at": { + "name": "unbanned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "player_bans_player_id_mc_players_id_fk": { + "name": "player_bans_player_id_mc_players_id_fk", + "tableFrom": "player_bans", + "tableTo": "mc_players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "player_bans_banned_by_users_id_fk": { + "name": "player_bans_banned_by_users_id_fk", + "tableFrom": "player_bans", + "tableTo": "users", + "columnsFrom": [ + "banned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "player_bans_unbanned_by_users_id_fk": { + "name": "player_bans_unbanned_by_users_id_fk", + "tableFrom": "player_bans", + "tableTo": "users", + "columnsFrom": [ + "unbanned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "player_chat_history": { + "name": "player_chat_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "player_chat_history_player_id_mc_players_id_fk": { + "name": "player_chat_history_player_id_mc_players_id_fk", + "tableFrom": "player_chat_history", + "tableTo": "mc_players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "player_spawn_points": { + "name": "player_spawn_points", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "player_id": { + "name": "player_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "world": { + "name": "world", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x": { + "name": "x", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y": { + "name": "y", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "z": { + "name": "z", + "type": "real", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "player_spawn_points_player_id_mc_players_id_fk": { + "name": "player_spawn_points_player_id_mc_players_id_fk", + "tableFrom": "player_spawn_points", + "tableTo": "mc_players", + "columnsFrom": [ + "player_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "plugins": { + "name": "plugins", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "jar_file": { + "name": "jar_file", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "installed_at": { + "name": "installed_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scheduled_tasks": { + "name": "scheduled_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_enabled": { + "name": "is_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "last_run": { + "name": "last_run", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "next_run": { + "name": "next_run", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "server_settings": { + "name": "server_settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "minecraft_path": { + "name": "minecraft_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_jar": { + "name": "server_jar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_version": { + "name": "server_version", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "server_type": { + "name": "server_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_ram": { + "name": "max_ram", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 4096 + }, + "min_ram": { + "name": "min_ram", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 1024 + }, + "rcon_enabled": { + "name": "rcon_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "rcon_port": { + "name": "rcon_port", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 25575 + }, + "rcon_password": { + "name": "rcon_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "java_args": { + "name": "java_args", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "auto_start": { + "name": "auto_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "restart_on_crash": { + "name": "restart_on_crash", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backup_enabled": { + "name": "backup_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "backup_schedule": { + "name": "backup_schedule", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bluemap_enabled": { + "name": "bluemap_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "bluemap_url": { + "name": "bluemap_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'moderator'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verifications": { + "name": "verifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index f042696..5ffd025 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1772980984285, "tag": "0000_overjoyed_thundra", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1772984147555, + "tag": "0001_gifted_loa", + "breakpoints": true } ] } \ No newline at end of file diff --git a/lib/auth/client.ts b/lib/auth/client.ts index 678d0b4..1d4324b 100644 --- a/lib/auth/client.ts +++ b/lib/auth/client.ts @@ -10,7 +10,11 @@ import type { Auth } from "./index"; * code. Mirrors the plugins registered on the server-side `auth` instance. */ export const authClient = createAuthClient({ - baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL ?? "http://localhost:3000", + // No baseURL — uses window.location.origin automatically, which always + // produces same-origin requests and avoids CSP connect-src issues. + ...(process.env.NEXT_PUBLIC_BETTER_AUTH_URL + ? { baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL } + : {}), plugins: [ // Enables organization.* methods (createOrganization, getActiveMember, etc.) diff --git a/lib/auth/index.ts b/lib/auth/index.ts index 0db9b59..203a843 100644 --- a/lib/auth/index.ts +++ b/lib/auth/index.ts @@ -2,6 +2,7 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { organization } from "better-auth/plugins"; import { magicLink } from "better-auth/plugins/magic-link"; +import { count, eq } from "drizzle-orm"; import { db } from "@/lib/db"; import * as schema from "@/lib/db/schema"; @@ -19,13 +20,14 @@ export const auth = betterAuth({ // ------------------------------------------------------------------------- database: drizzleAdapter(db, { provider: "sqlite", + // Keys must match Better Auth's internal model names (singular). + // usePlural: false (default) → "user", "session", "account", "verification" schema: { - users: schema.users, - sessions: schema.sessions, - accounts: schema.accounts, - verifications: schema.verifications, + user: schema.users, + session: schema.sessions, + account: schema.accounts, + verification: schema.verifications, }, - usePlural: false, }), // ------------------------------------------------------------------------- @@ -52,6 +54,30 @@ export const auth = betterAuth({ maxPasswordLength: 128, }, + // ------------------------------------------------------------------------- + // Database hooks — first registered user becomes admin automatically + // ------------------------------------------------------------------------- + databaseHooks: { + user: { + create: { + after: async (user) => { + // Count all users; if this is the very first, promote to admin + const [{ total }] = await db + .select({ total: count() }) + .from(schema.users); + + if (total === 1) { + await db + .update(schema.users) + .set({ role: "admin" } as Record) + .where(eq(schema.users.id, user.id)); + console.log(`[Auth] First user ${user.id} (${user.email}) promoted to admin`); + } + }, + }, + }, + }, + // ------------------------------------------------------------------------- // Plugins // ------------------------------------------------------------------------- @@ -104,5 +130,18 @@ export type Auth = typeof auth; /** The server-side session type returned by auth.api.getSession */ export type Session = typeof auth.$Infer.Session.session; -/** The user type embedded in every session */ -export type User = typeof auth.$Infer.Session.user; +/** The user type embedded in every session, with custom additionalFields */ +export type User = typeof auth.$Infer.Session.user & { + role?: "superadmin" | "admin" | "moderator" | null; +}; + +type RawSession = NonNullable>>; + +/** Typed wrapper around auth.api.getSession that includes the role field */ +export async function getAuthSession( + headers: Headers, +): Promise<(Omit & { user: User }) | null> { + return auth.api.getSession({ headers }) as Promise< + (Omit & { user: User }) | null + >; +} diff --git a/lib/db/schema.ts b/lib/db/schema.ts index a95b0e6..395d9c9 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -24,8 +24,8 @@ export const users = sqliteTable("users", { }) .notNull() .default("moderator"), - createdAt: integer("created_at").notNull(), - updatedAt: integer("updated_at").notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(), }); export const sessions = sqliteTable("sessions", { @@ -34,11 +34,11 @@ export const sessions = sqliteTable("sessions", { .notNull() .references(() => users.id, { onDelete: "cascade" }), token: text("token").notNull().unique(), - expiresAt: integer("expires_at").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), - createdAt: integer("created_at").notNull(), - updatedAt: integer("updated_at").notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(), }); export const accounts = sqliteTable("accounts", { @@ -50,18 +50,20 @@ export const accounts = sqliteTable("accounts", { providerId: text("provider_id").notNull(), accessToken: text("access_token"), refreshToken: text("refresh_token"), - expiresAt: integer("expires_at"), - createdAt: integer("created_at").notNull(), - updatedAt: integer("updated_at").notNull(), + idToken: text("id_token"), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }), + password: text("password"), // hashed password for email/password auth + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(), }); export const verifications = sqliteTable("verifications", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), - expiresAt: integer("expires_at").notNull(), - createdAt: integer("created_at").notNull(), - updatedAt: integer("updated_at").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(), }); // --------------------------------------------------------------------------- diff --git a/next.config.ts b/next.config.ts index 3ae42e6..b299518 100644 --- a/next.config.ts +++ b/next.config.ts @@ -28,76 +28,28 @@ const nextConfig: NextConfig = { ], }, + // Security headers (CSP + non-CSP) are applied by proxy.ts so they can + // include a per-request nonce. Only static headers that don't conflict are + // set here for paths the middleware doesn't cover (e.g. _next/static). async headers() { - const cspDirectives = [ - "default-src 'self'", - // Scripts: self + strict-dynamic (Turbopack compatible) - "script-src 'self' 'unsafe-inline'", - // Styles: self + unsafe-inline (required for Tailwind/CSS-in-JS in Next.js) - "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", - // Fonts - "font-src 'self' https://fonts.gstatic.com data:", - // Images: self + data URIs + MC avatar APIs - "img-src 'self' data: blob: https://crafatar.com https://mc-heads.net https://visage.surgeplay.com https://minotar.net", - // Connect: self + WebSocket for Socket.io - "connect-src 'self' ws: wss:", - // Frames: allow same-origin (BlueMap) + configurable origins - "frame-src 'self'", - // Frame ancestors: only same origin (replaces X-Frame-Options) - "frame-ancestors 'self'", - // Workers: self + blob (xterm.js, Monaco) - "worker-src 'self' blob:", - // Media - "media-src 'self'", - // Manifest - "manifest-src 'self'", - // Object: none - "object-src 'none'", - // Base URI - "base-uri 'self'", - // Form actions - "form-action 'self'", - // Upgrade insecure requests in production - ...(process.env.NODE_ENV === "production" - ? ["upgrade-insecure-requests"] - : []), - ].join("; "); - - const securityHeaders = [ - { - key: "Content-Security-Policy", - value: cspDirectives, - }, - { - key: "X-Frame-Options", - value: "SAMEORIGIN", - }, - { - key: "X-Content-Type-Options", - value: "nosniff", - }, - { - key: "Referrer-Policy", - value: "strict-origin-when-cross-origin", - }, - { - key: "Permissions-Policy", - value: "camera=(), microphone=(), geolocation=(), browsing-topics=()", - }, - { - key: "X-DNS-Prefetch-Control", - value: "on", - }, - { - key: "Strict-Transport-Security", - value: "max-age=63072000; includeSubDomains; preload", - }, - ]; - return [ { source: "/(.*)", - headers: securityHeaders, + headers: [ + // CSP is intentionally omitted here — proxy.ts owns it. + { key: "X-Frame-Options", value: "SAMEORIGIN" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=(), browsing-topics=()", + }, + { key: "X-DNS-Prefetch-Control", value: "on" }, + { + key: "Strict-Transport-Security", + value: "max-age=63072000; includeSubDomains; preload", + }, + ], }, ]; }, diff --git a/proxy.ts b/proxy.ts index 8cb600c..a76ccb6 100644 --- a/proxy.ts +++ b/proxy.ts @@ -51,14 +51,24 @@ function isPublicPath(pathname: string): boolean { // --------------------------------------------------------------------------- // Security headers applied to every response // --------------------------------------------------------------------------- +const isDev = process.env.NODE_ENV !== "production"; + function buildCSP(nonce: string): string { + // In dev, Next.js hot-reload and some auth libs require 'unsafe-eval'. + // In production we restrict to 'wasm-unsafe-eval' (WebAssembly only). + const evalDirective = isDev ? "'unsafe-eval'" : "'wasm-unsafe-eval'"; + return [ "default-src 'self'", - `script-src 'self' 'nonce-${nonce}'`, + `script-src 'self' 'nonce-${nonce}' ${evalDirective} 'unsafe-inline'`, "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", "font-src 'self' https://fonts.gstatic.com data:", "img-src 'self' data: blob: https://crafatar.com https://mc-heads.net https://visage.surgeplay.com https://minotar.net", - "connect-src 'self' ws: wss:", + // In dev, include http://localhost:* explicitly so absolute-URL fetches + // (e.g. from Better Auth client) aren't blocked by a strict 'self' check. + isDev + ? "connect-src 'self' http://localhost:* ws://localhost:* wss://localhost:* ws: wss:" + : "connect-src 'self' ws: wss:", "frame-src 'self'", "frame-ancestors 'self'", "worker-src 'self' blob:", diff --git a/types/better-auth.d.ts b/types/better-auth.d.ts new file mode 100644 index 0000000..e430317 --- /dev/null +++ b/types/better-auth.d.ts @@ -0,0 +1,11 @@ +/** + * Augment Better Auth's session user type to include the `role` additional field + * defined in lib/auth/index.ts. + */ +declare module "better-auth" { + interface UserAdditionalFields { + role?: string | null; + } +} + +export {};