BugFixes galore

This commit is contained in:
2026-03-08 17:01:36 +01:00
parent 781f0f14fa
commit c8895c8e80
39 changed files with 2255 additions and 237 deletions

View File

@@ -315,8 +315,19 @@ function LoginPageInner() {
</form>
</div>
{/* Register link */}
<p className="mt-4 text-center text-xs text-zinc-500">
No account?{" "}
<Link
href="/register"
className="text-zinc-300 transition-colors hover:text-white focus-visible:outline-none focus-visible:underline"
>
Create one
</Link>
</p>
{/* Footer */}
<p className="mt-6 text-center text-[11px] text-zinc-600">
<p className="mt-4 text-center text-[11px] text-zinc-600">
CubeAdmin &mdash; Secure server management
</p>
</div>

View File

@@ -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<typeof registerSchema>;
type FieldErrors = Partial<Record<keyof RegisterFormValues, string>>;
function CubeIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden="true"
>
<polygon points="16,4 28,10 16,16 4,10" fill="#059669" opacity="0.9" />
<polygon points="4,10 16,16 16,28 4,22" fill="#047857" opacity="0.95" />
<polygon points="28,10 16,16 16,28 28,22" fill="#10b981" opacity="0.85" />
</svg>
);
}
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 (
<div className="flex flex-col gap-1.5">
<Label htmlFor={id} className="text-xs font-medium text-zinc-300">
{label}
</Label>
<div className="relative">
<Input
id={id}
type={type}
value={value}
onChange={(e) => 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}
</div>
{error && (
<p
id={`${id}-error`}
className="flex items-center gap-1 text-xs text-red-400"
role="alert"
>
<AlertCircle className="h-3 w-3 flex-shrink-0" />
{error}
</p>
)}
</div>
);
}
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<FieldErrors>({});
const [globalError, setGlobalError] = useState<string | null>(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<HTMLFormElement>) {
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 (
<div className="flex min-h-screen flex-col items-center justify-center bg-zinc-950 px-4">
{/* Background grid */}
<div
className="pointer-events-none fixed inset-0 bg-[size:32px_32px] opacity-[0.02]"
style={{
backgroundImage:
"linear-gradient(to right, #ffffff 1px, transparent 1px), linear-gradient(to bottom, #ffffff 1px, transparent 1px)",
}}
aria-hidden="true"
/>
{/* Radial glow */}
<div
className="pointer-events-none fixed inset-0 flex items-center justify-center"
aria-hidden="true"
>
<div className="h-[500px] w-[500px] rounded-full bg-emerald-500/5 blur-3xl" />
</div>
<div className="relative z-10 w-full max-w-sm">
{/* Logo */}
<div className="mb-8 flex flex-col items-center gap-3">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-zinc-900 ring-1 ring-white/[0.08]">
<CubeIcon className="h-9 w-9" />
</div>
<div className="text-center">
<h1 className="text-xl font-semibold tracking-tight text-zinc-100">
CubeAdmin
</h1>
<p className="mt-0.5 text-xs text-zinc-500">
Minecraft Server Management
</p>
</div>
</div>
{/* Card */}
<div className="rounded-xl bg-zinc-900/50 p-6 ring-1 ring-white/[0.08] backdrop-blur-sm">
<div className="mb-5">
<h2 className="text-base font-semibold text-zinc-100">
Create an account
</h2>
<p className="mt-1 text-xs text-zinc-500">
The first account registered becomes the administrator.
</p>
</div>
{globalError && (
<div
className="mb-4 flex items-start gap-2.5 rounded-lg bg-red-500/10 px-3 py-2.5 ring-1 ring-red-500/20"
role="alert"
aria-live="assertive"
>
<AlertCircle className="mt-0.5 h-4 w-4 flex-shrink-0 text-red-400" />
<p className="text-xs text-red-300">{globalError}</p>
</div>
)}
<form onSubmit={handleSubmit} noValidate className="flex flex-col gap-4">
<FormField
id="name"
label="Display name"
value={name}
onChange={setName}
error={fieldErrors.name}
placeholder="Your name"
autoComplete="name"
disabled={isPending}
/>
<FormField
id="email"
label="Email address"
type="email"
value={email}
onChange={setEmail}
error={fieldErrors.email}
placeholder="admin@example.com"
autoComplete="email"
disabled={isPending}
/>
<FormField
id="password"
label="Password"
type={showPassword ? "text" : "password"}
value={password}
onChange={setPassword}
error={fieldErrors.password}
placeholder="At least 8 characters"
autoComplete="new-password"
disabled={isPending}
>
<button
type="button"
onClick={() => setShowPassword((v) => !v)}
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded text-zinc-500 transition-colors hover:text-zinc-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/50"
aria-label={showPassword ? "Hide password" : "Show password"}
tabIndex={-1}
>
{showPassword ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
</button>
</FormField>
<FormField
id="confirm"
label="Confirm password"
type={showConfirm ? "text" : "password"}
value={confirm}
onChange={setConfirm}
error={fieldErrors.confirm}
placeholder="Repeat your password"
autoComplete="new-password"
disabled={isPending}
>
<button
type="button"
onClick={() => setShowConfirm((v) => !v)}
className="absolute right-2.5 top-1/2 -translate-y-1/2 rounded text-zinc-500 transition-colors hover:text-zinc-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500/50"
aria-label={showConfirm ? "Hide password" : "Show password"}
tabIndex={-1}
>
{showConfirm ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
</button>
</FormField>
<Button
type="submit"
disabled={isPending}
className="mt-1 h-9 w-full bg-emerald-600 text-white hover:bg-emerald-500 focus-visible:ring-emerald-500/50 disabled:opacity-60 border-0 font-medium"
>
{isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Creating account
</>
) : (
"Create account"
)}
</Button>
</form>
</div>
<p className="mt-4 text-center text-xs text-zinc-500">
Already have an account?{" "}
<Link
href="/login"
className="text-zinc-300 transition-colors hover:text-white focus-visible:outline-none focus-visible:underline"
>
Sign in
</Link>
</p>
</div>
</div>
);
}

View File

@@ -69,8 +69,9 @@ export default function ConsolePage() {
});
// Receive buffered history on connect
socket.on("history", (data: { lines: string[] }) => {
const historicalLines = data.lines.map((line) => ({
socket.on("history", (data: string[] | { lines: string[] }) => {
const rawLines = Array.isArray(data) ? data : (data?.lines ?? []);
const historicalLines = rawLines.map((line) => ({
text: line,
timestamp: Date.now(),
type: classifyLine(line) as LogLine["type"],

View File

@@ -0,0 +1,183 @@
"use client";
import { useState, useTransition } from "react";
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 { Separator } from "@/components/ui/separator";
import { User, Lock, Shield } from "lucide-react";
import { toast } from "sonner";
import { authClient, useSession } from "@/lib/auth/client";
export default function SettingsPage() {
const { data: session, isPending } = useSession();
const [name, setName] = useState("");
const [nameLoading, startNameTransition] = useTransition();
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [passwordLoading, startPasswordTransition] = useTransition();
// Populate name field once session loads
const displayName = name || session?.user?.name || "";
function handleNameSave() {
if (!displayName.trim()) {
toast.error("Name cannot be empty");
return;
}
startNameTransition(async () => {
const { error } = await authClient.updateUser({ name: displayName.trim() });
if (error) {
toast.error(error.message ?? "Failed to update name");
} else {
toast.success("Display name updated");
}
});
}
function handlePasswordChange() {
if (!currentPassword) {
toast.error("Current password is required");
return;
}
if (newPassword.length < 8) {
toast.error("New password must be at least 8 characters");
return;
}
if (newPassword !== confirmPassword) {
toast.error("Passwords do not match");
return;
}
startPasswordTransition(async () => {
const { error } = await authClient.changePassword({
currentPassword,
newPassword,
revokeOtherSessions: false,
});
if (error) {
toast.error(error.message ?? "Failed to change password");
} else {
toast.success("Password changed successfully");
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
}
});
}
return (
<div className="p-6 max-w-2xl space-y-6">
<div>
<h1 className="text-2xl font-bold text-white">Account Settings</h1>
<p className="text-zinc-400 text-sm mt-1">Manage your profile and security preferences</p>
</div>
{/* Profile */}
<Card className="bg-zinc-900 border-zinc-800">
<CardHeader>
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
<User className="w-4 h-4 text-emerald-500" />
Profile
</CardTitle>
<CardDescription className="text-zinc-500">
Update your display name and view account info
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1.5">
<Label className="text-zinc-300">Email</Label>
<Input
value={session?.user?.email ?? ""}
disabled
className="bg-zinc-800 border-zinc-700 text-zinc-400 cursor-not-allowed"
/>
<p className="text-xs text-zinc-500">Email address cannot be changed</p>
</div>
<div className="space-y-1.5">
<Label className="text-zinc-300">Display Name</Label>
<Input
value={name || session?.user?.name || ""}
onChange={(e) => setName(e.target.value)}
placeholder="Your name"
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
disabled={isPending}
/>
</div>
{session?.user && (
<div className="flex items-center gap-2 text-xs text-zinc-500">
<Shield className="w-3 h-3" />
Role: <span className="text-zinc-300 capitalize">{(session.user as { role?: string }).role ?? "moderator"}</span>
</div>
)}
<Button
onClick={handleNameSave}
disabled={nameLoading || isPending}
className="bg-emerald-600 hover:bg-emerald-500 text-white"
size="sm"
>
{nameLoading ? "Saving…" : "Save Name"}
</Button>
</CardContent>
</Card>
<Separator className="bg-zinc-800" />
{/* Password */}
<Card className="bg-zinc-900 border-zinc-800">
<CardHeader>
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
<Lock className="w-4 h-4 text-emerald-500" />
Change Password
</CardTitle>
<CardDescription className="text-zinc-500">
Choose a strong password with at least 8 characters
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-1.5">
<Label className="text-zinc-300">Current Password</Label>
<Input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
placeholder="••••••••"
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
/>
</div>
<div className="space-y-1.5">
<Label className="text-zinc-300">New Password</Label>
<Input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="••••••••"
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
/>
</div>
<div className="space-y-1.5">
<Label className="text-zinc-300">Confirm New Password</Label>
<Input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="••••••••"
className="bg-zinc-800 border-zinc-700 text-white placeholder:text-zinc-500"
/>
</div>
<Button
onClick={handlePasswordChange}
disabled={passwordLoading}
className="bg-emerald-600 hover:bg-emerald-500 text-white"
size="sm"
>
{passwordLoading ? "Changing…" : "Change Password"}
</Button>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,254 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Download, RefreshCw, AlertTriangle, CheckCircle2, Server } from "lucide-react";
import { toast } from "sonner";
const SERVER_TYPES = ["vanilla", "paper", "fabric"] as const;
type ServerType = (typeof SERVER_TYPES)[number];
interface Settings {
serverType?: string;
serverVersion?: string;
serverJar?: string;
}
export default function UpdatesPage() {
const [settings, setSettings] = useState<Settings | null>(null);
const [selectedType, setSelectedType] = useState<ServerType>("paper");
const [versions, setVersions] = useState<string[]>([]);
const [selectedVersion, setSelectedVersion] = useState("");
const [loadingSettings, setLoadingSettings] = useState(true);
const [loadingVersions, setLoadingVersions] = useState(false);
const [saving, setSaving] = useState(false);
const fetchSettings = useCallback(async () => {
try {
const res = await fetch("/api/server/settings");
if (res.ok) {
const data = await res.json();
if (data.settings) {
setSettings(data.settings);
setSelectedType((data.settings.serverType as ServerType) ?? "paper");
setSelectedVersion(data.settings.serverVersion ?? "");
}
}
} finally {
setLoadingSettings(false);
}
}, []);
const fetchVersions = useCallback(async (type: string) => {
setLoadingVersions(true);
setVersions([]);
try {
const res = await fetch(`/api/server/versions?type=${type}`);
if (res.ok) {
const data = await res.json();
setVersions(data.versions ?? []);
} else {
toast.error("Failed to fetch versions");
}
} catch {
toast.error("Network error fetching versions");
} finally {
setLoadingVersions(false);
}
}, []);
useEffect(() => {
fetchSettings();
}, [fetchSettings]);
useEffect(() => {
fetchVersions(selectedType);
}, [selectedType, fetchVersions]);
const isUpToDate =
settings?.serverVersion === selectedVersion &&
settings?.serverType === selectedType;
async function handleApply() {
if (!selectedVersion) {
toast.error("Please select a version");
return;
}
setSaving(true);
try {
const res = await fetch("/api/server/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ serverType: selectedType, serverVersion: selectedVersion }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error ?? "Failed to apply");
}
setSettings((prev) => ({ ...prev, serverType: selectedType, serverVersion: selectedVersion }));
toast.success(`Server version set to ${selectedType} ${selectedVersion}`);
} catch (err) {
toast.error(err instanceof Error ? err.message : "Failed to apply version");
} finally {
setSaving(false);
}
}
const currentVersion = settings?.serverVersion
? `${settings.serverType ?? "unknown"} ${settings.serverVersion}`
: "Not configured";
return (
<div className="p-6 max-w-2xl space-y-6">
<div>
<h1 className="text-2xl font-bold text-white">Server Updates</h1>
<p className="text-zinc-400 text-sm mt-1">
Manage your Minecraft server version
</p>
</div>
{/* Current version */}
<Card className="bg-zinc-900 border-zinc-800">
<CardHeader>
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
<Server className="w-4 h-4 text-emerald-500" />
Current Version
</CardTitle>
</CardHeader>
<CardContent>
{loadingSettings ? (
<Skeleton className="h-6 w-48 bg-zinc-800" />
) : (
<div className="flex items-center gap-3">
<span className="text-white font-mono text-sm">{currentVersion}</span>
{settings?.serverVersion && (
<Badge
className={
isUpToDate
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/20"
: "bg-amber-500/10 text-amber-400 border-amber-500/20"
}
>
{isUpToDate ? "Up to date" : "Update available"}
</Badge>
)}
</div>
)}
</CardContent>
</Card>
{/* Version picker */}
<Card className="bg-zinc-900 border-zinc-800">
<CardHeader>
<CardTitle className="text-base font-medium text-zinc-300 flex items-center gap-2">
<Download className="w-4 h-4 text-emerald-500" />
Select Version
</CardTitle>
<CardDescription className="text-zinc-500">
Choose a server type and version to apply
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className="text-sm text-zinc-300">Server Type</label>
<Select
value={selectedType}
onValueChange={(v) => { if (v) setSelectedType(v as ServerType); }}
>
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-zinc-800 border-zinc-700">
{SERVER_TYPES.map((t) => (
<SelectItem key={t} value={t} className="text-zinc-300 focus:bg-zinc-700 focus:text-white capitalize">
{t.charAt(0).toUpperCase() + t.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<label className="text-sm text-zinc-300">Version</label>
<Select
value={selectedVersion}
onValueChange={(v) => { if (v) setSelectedVersion(v); }}
disabled={loadingVersions || versions.length === 0}
>
<SelectTrigger className="bg-zinc-800 border-zinc-700 text-white">
<SelectValue
placeholder={
loadingVersions
? "Loading…"
: versions.length === 0
? "No versions found"
: "Select version"
}
/>
</SelectTrigger>
<SelectContent className="bg-zinc-800 border-zinc-700 max-h-64">
{versions.map((v) => (
<SelectItem key={v} value={v} className="text-zinc-300 focus:bg-zinc-700 focus:text-white font-mono text-sm">
{v}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-2">
<Button
onClick={handleApply}
disabled={saving || !selectedVersion || loadingVersions}
className="bg-emerald-600 hover:bg-emerald-500 text-white"
size="sm"
>
<Download className="w-3.5 h-3.5 mr-1.5" />
{saving ? "Applying…" : "Apply Version"}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => fetchVersions(selectedType)}
disabled={loadingVersions}
className="border-zinc-700 text-zinc-300 hover:bg-zinc-800"
>
<RefreshCw className={`w-3.5 h-3.5 mr-1.5 ${loadingVersions ? "animate-spin" : ""}`} />
Refresh
</Button>
</div>
{selectedVersion && settings?.serverVersion && selectedVersion !== settings.serverVersion && (
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-400 text-sm">
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0" />
<p>
Changing from <span className="font-mono">{settings.serverVersion}</span> to{" "}
<span className="font-mono">{selectedVersion}</span> requires a server restart.
Make sure to create a backup first.
</p>
</div>
)}
{isUpToDate && selectedVersion && (
<div className="flex items-center gap-2 rounded-lg border border-emerald-500/30 bg-emerald-500/10 p-3 text-emerald-400 text-sm">
<CheckCircle2 className="w-4 h-4 shrink-0" />
<p>This version is already configured.</p>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -1,12 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { db } from "@/lib/db";
import { auditLogs, users } from "@/lib/db/schema";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
import { desc, eq, like, and, gte, lte } from "drizzle-orm";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -0,0 +1,4 @@
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { deleteBackup } from "@/lib/backup/manager";
import { db } from "@/lib/db";
import { backups } from "@/lib/db/schema";
@@ -10,7 +10,7 @@ export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
@@ -30,7 +30,7 @@ export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
import { createBackup, listBackups, BackupType } from "@/lib/backup/manager";
import { z } from "zod";
@@ -9,7 +9,7 @@ const CreateBackupSchema = z.object({
});
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const ip = getClientIp(req);
@@ -21,7 +21,7 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { sanitizeFilePath } from "@/lib/security/sanitize";
import { db } from "@/lib/db";
import { auditLogs } from "@/lib/db/schema";
@@ -9,7 +9,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
export async function DELETE(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -1,11 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { sanitizeFilePath } from "@/lib/security/sanitize";
import * as fs from "node:fs";
import * as path from "node:path";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const mcBase = path.resolve(process.env.MC_SERVER_PATH ?? "/opt/minecraft/server");

View File

@@ -1,12 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
import { sanitizeFilePath } from "@/lib/security/sanitize";
import * as fs from "node:fs";
import * as path from "node:path";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const ip = getClientIp(req);

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
import { sanitizeFilePath } from "@/lib/security/sanitize";
import { db } from "@/lib/db";
@@ -12,7 +12,7 @@ const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500 MB
const BLOCKED_EXTENSIONS = new Set([".exe", ".bat", ".cmd", ".sh", ".ps1"]);
export async function POST(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -1,11 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { mcProcessManager } from "@/lib/minecraft/process";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
import * as os from "node:os";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const ip = getClientIp(req);

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { db } from "@/lib/db";
import { mcPlayers, playerBans, playerChatHistory, playerSpawnPoints, auditLogs } from "@/lib/db/schema";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
@@ -13,7 +13,7 @@ export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { id } = await params;
@@ -38,7 +38,7 @@ export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin", "moderator"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -1,12 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { db } from "@/lib/db";
import { mcPlayers } from "@/lib/db/schema";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
import { desc, like, or, eq } from "drizzle-orm";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const ip = getClientIp(req);

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { db } from "@/lib/db";
import { plugins, auditLogs } from "@/lib/db/schema";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
@@ -11,7 +11,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const ip = getClientIp(req);
@@ -36,7 +36,7 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { db } from "@/lib/db";
import { scheduledTasks } from "@/lib/db/schema";
import { scheduleTask, stopTask } from "@/lib/scheduler";
@@ -19,7 +19,7 @@ export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
@@ -56,7 +56,7 @@ export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { db } from "@/lib/db";
import { scheduledTasks } from "@/lib/db/schema";
import { scheduleTask, stopTask } from "@/lib/scheduler";
@@ -18,7 +18,7 @@ const TaskSchema = z.object({
});
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const tasks = await db.select().from(scheduledTasks).orderBy(scheduledTasks.createdAt);
@@ -26,7 +26,7 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (!["superadmin", "admin"].includes(session.user.role ?? "")) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { mcProcessManager } from "@/lib/minecraft/process";
import { db } from "@/lib/db";
import { auditLogs } from "@/lib/db/schema";
@@ -14,7 +14,7 @@ const ActionSchema = z.object({
export async function POST(req: NextRequest) {
// Auth
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { db } from "@/lib/db";
import { serverSettings } from "@/lib/db/schema";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
@@ -25,7 +25,7 @@ const UpdateSettingsSchema = z.object({
});
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const settings = await db.select().from(serverSettings).get();
@@ -38,7 +38,7 @@ export async function GET(req: NextRequest) {
}
export async function PATCH(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (session.user.role !== "superadmin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -1,10 +1,10 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { mcProcessManager } from "@/lib/minecraft/process";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

View File

@@ -1,10 +1,10 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { fetchVanillaVersions, fetchPaperVersions, fetchFabricVersions, type VersionInfo } from "@/lib/minecraft/versions";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const ip = getClientIp(req);

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { auth, getAuthSession } from "@/lib/auth";
import { db } from "@/lib/db";
import { users, invitations } from "@/lib/db/schema";
import { checkRateLimit, getClientIp } from "@/lib/security/rateLimit";
@@ -15,7 +15,7 @@ const InviteSchema = z.object({
});
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (session.user.role !== "superadmin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
@@ -35,7 +35,7 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
const session = await auth.api.getSession({ headers: req.headers });
const session = await getAuthSession(req.headers);
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
if (session.user.role !== "superadmin") {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });

View File

@@ -1,65 +1,5 @@
import Image from "next/image";
import { redirect } from "next/navigation";
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
export default function RootPage() {
redirect("/dashboard");
}