This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
import { FolderKanban, ImageIcon, LockKeyhole, ShieldAlert } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { Container } from "@/components/layout/container";
|
||||
import { StatsCard } from "@/components/dashboard/stats-card";
|
||||
import { MotionFade } from "@/components/motion-fade";
|
||||
import { AdminDashboardShell } from "@/components/admin/admin-dashboard-shell";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
clearAdminSessionCookie,
|
||||
getAdminLockState,
|
||||
isAdminAuthConfigured,
|
||||
isAdminAuthenticated,
|
||||
isPasswordValid,
|
||||
registerFailedAdminAttempt,
|
||||
resetAdminFailedAttempts,
|
||||
setAdminSessionCookie,
|
||||
} from "@/lib/admin-auth";
|
||||
import { getMaintenanceMode } from "@/lib/app-config";
|
||||
import { getAdminMediaAssets } from "@/lib/media";
|
||||
import { getAdminPortfolioCategories, getAdminPortfolioProjects } from "@/lib/portfolio";
|
||||
|
||||
type AdminPageProps = {
|
||||
searchParams?: Promise<{
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const copy = {
|
||||
title: "Uebersicht",
|
||||
subtitle: "Kompakter Status zu Portfolio, Media und Wartung.",
|
||||
overview: "Uebersicht",
|
||||
maintenance: "Wartungsmodus",
|
||||
media: "Media",
|
||||
siteSettings: "Settings",
|
||||
portfolio: "Portfolio",
|
||||
uiKit: "UI Kit",
|
||||
logout: "Ausloggen",
|
||||
backToSite: "Zur Website",
|
||||
loginTitle: "Admin Login",
|
||||
loginText: "Nur autorisierte Nutzer duerfen diesen Bereich verwenden.",
|
||||
passwordLabel: "Passwort",
|
||||
loginButton: "Einloggen",
|
||||
invalidLogin: "Falsches Passwort.",
|
||||
lockedLogin: "Zu viele Fehlversuche. Bitte spaeter erneut versuchen.",
|
||||
configMissing: "ADMIN_PASSWORD und ADMIN_AUTH_SECRET fehlen in env.",
|
||||
basicAuthMissing: "ADMIN_BASIC_AUTH_USER und ADMIN_BASIC_AUTH_PASS fehlen in env.",
|
||||
maintenanceOn: "Aktiv",
|
||||
maintenanceOff: "Inaktiv",
|
||||
portfolioOnline: "Portfolio online",
|
||||
mediaImages: "Media Images",
|
||||
maintenanceStatus: "Maintenance",
|
||||
drafts: "Drafts",
|
||||
activeCategories: "Active Categories",
|
||||
};
|
||||
|
||||
export default async function AdminPage({ searchParams }: AdminPageProps) {
|
||||
const resolvedSearchParams = await searchParams;
|
||||
const authConfigured = isAdminAuthConfigured();
|
||||
const basicConfigured = Boolean(
|
||||
process.env.ADMIN_BASIC_AUTH_USER && process.env.ADMIN_BASIC_AUTH_PASS,
|
||||
);
|
||||
const authenticated = await isAdminAuthenticated();
|
||||
const lockState = await getAdminLockState();
|
||||
|
||||
async function loginAction(formData: FormData) {
|
||||
"use server";
|
||||
|
||||
const password = String(formData.get("password") ?? "");
|
||||
const currentLockState = await getAdminLockState();
|
||||
|
||||
if (currentLockState.locked) {
|
||||
redirect("/?error=locked");
|
||||
}
|
||||
|
||||
if (!isAdminAuthConfigured() || !isPasswordValid(password)) {
|
||||
const failState = await registerFailedAdminAttempt();
|
||||
if (failState.locked) {
|
||||
redirect("/?error=locked");
|
||||
}
|
||||
|
||||
redirect("/?error=invalid");
|
||||
}
|
||||
|
||||
await resetAdminFailedAttempts();
|
||||
await setAdminSessionCookie();
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
async function logoutAction() {
|
||||
"use server";
|
||||
|
||||
await clearAdminSessionCookie();
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<Container size="narrow" className="py-12">
|
||||
<MotionFade>
|
||||
<Card className="border-border/70 bg-card/95 shadow-sm">
|
||||
<CardHeader>
|
||||
<p className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
||||
<LockKeyhole className="h-4 w-4 text-brand-primary" />
|
||||
{copy.loginTitle}
|
||||
</p>
|
||||
<CardDescription>{copy.loginText}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!authConfigured ? (
|
||||
<p className="mb-4 rounded-nested border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{copy.configMissing}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{!basicConfigured ? (
|
||||
<p className="mb-4 rounded-nested border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
{copy.basicAuthMissing}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{resolvedSearchParams?.error === "invalid" ? (
|
||||
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
||||
{copy.invalidLogin}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{resolvedSearchParams?.error === "locked" || lockState.locked ? (
|
||||
<p className="mb-4 rounded-nested border border-status-warning/30 bg-status-warning-soft px-3 py-2 text-sm text-status-warning">
|
||||
{copy.lockedLogin}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<form action={loginAction} className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password" className="sr-only">
|
||||
{copy.passwordLabel}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<LockKeyhole className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
placeholder={copy.passwordLabel}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!authConfigured || !basicConfigured || lockState.locked}
|
||||
>
|
||||
<LockKeyhole className="h-4 w-4" />
|
||||
{copy.loginButton}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</MotionFade>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const [maintenanceEnabled, categories, projects, mediaAssets] = await Promise.all([
|
||||
getMaintenanceMode(),
|
||||
getAdminPortfolioCategories(),
|
||||
getAdminPortfolioProjects(),
|
||||
getAdminMediaAssets(),
|
||||
]);
|
||||
|
||||
const publishedProjects = projects.filter((project) => project.isPublished).length;
|
||||
const draftProjects = projects.length - publishedProjects;
|
||||
const activeCategories = categories.filter((category) => category.isActive).length;
|
||||
const imageCount = mediaAssets.filter((asset) => asset.kind === "IMAGE").length;
|
||||
|
||||
return (
|
||||
<AdminDashboardShell
|
||||
copy={copy}
|
||||
active="overview"
|
||||
logoutAction={logoutAction}
|
||||
headerTitle={copy.title}
|
||||
headerDescription={copy.subtitle}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<MotionFade delay={0.05}>
|
||||
<StatsCard
|
||||
title={copy.portfolioOnline}
|
||||
value={String(publishedProjects)}
|
||||
icon={FolderKanban}
|
||||
className="h-full"
|
||||
footer={(
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2 text-sm text-muted-foreground">
|
||||
<span>{copy.drafts}: {draftProjects}</span>
|
||||
<span>{copy.activeCategories}: {activeCategories}</span>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</MotionFade>
|
||||
|
||||
<MotionFade delay={0.1}>
|
||||
<StatsCard
|
||||
title={copy.mediaImages}
|
||||
value={String(imageCount)}
|
||||
icon={ImageIcon}
|
||||
description="Anzahl der Bilder innerhalb der Media Library."
|
||||
className="h-full"
|
||||
/>
|
||||
</MotionFade>
|
||||
|
||||
<MotionFade delay={0.15}>
|
||||
<StatsCard
|
||||
title={copy.maintenanceStatus}
|
||||
value={maintenanceEnabled ? copy.maintenanceOn : copy.maintenanceOff}
|
||||
icon={ShieldAlert}
|
||||
className="h-full"
|
||||
footer={(
|
||||
<div className="flex items-center justify-between gap-3 text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{maintenanceEnabled
|
||||
? "Website ist aktuell fuer Besucher gesperrt."
|
||||
: "Website ist aktuell offen."}
|
||||
</span>
|
||||
<Badge variant={maintenanceEnabled ? "warning" : "success"}>
|
||||
{maintenanceEnabled ? copy.maintenanceOn : copy.maintenanceOff}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</MotionFade>
|
||||
</section>
|
||||
</div>
|
||||
</AdminDashboardShell>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user