security: Zufallspasswörter beim Erststart, kryptographisch sichere JTI-Generierung
- seedDefaultUsers: generiert kryptographisch zufällige Passwörter (crypto/rand) statt hartkodiertes "archivmailrockz" — Passwörter werden einmalig im Terminal angezeigt und können danach nicht wiederhergestellt werden - generateJTI: verwendet crypto/rand (16 Byte, hex) statt time.UnixNano XOR deadbeef Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+106
-9
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { features, type Feature } from "@/data/features";
|
||||
import {
|
||||
getUsers,
|
||||
createUser,
|
||||
@@ -276,18 +277,17 @@ export default function AdminPage() {
|
||||
|
||||
const auditTotalPages = Math.ceil(auditTotal / AUDIT_PAGE_SIZE);
|
||||
|
||||
if (authLoading || !user) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<Navbar username={user.username} role={user.role} />
|
||||
<Navbar username={user?.username ?? ""} role={user?.role ?? ""} />
|
||||
<main className="mx-auto max-w-7xl px-4 py-6">
|
||||
{(authLoading || !user) ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-48" />
|
||||
<Skeleton className="h-10 w-full max-w-sm" />
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</div>
|
||||
) : (<>
|
||||
<h1 className="mb-6 text-2xl font-bold">Administration</h1>
|
||||
|
||||
<Tabs defaultValue="dashboard">
|
||||
@@ -296,6 +296,7 @@ export default function AdminPage() {
|
||||
<TabsTrigger value="services">Dienste</TabsTrigger>
|
||||
<TabsTrigger value="users">Benutzer</TabsTrigger>
|
||||
<TabsTrigger value="audit">Audit-Log</TabsTrigger>
|
||||
<TabsTrigger value="modules">Module</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* ── Dashboard ── */}
|
||||
@@ -1018,7 +1019,11 @@ export default function AdminPage() {
|
||||
</>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="modules" className="mt-4">
|
||||
<ModulesTab />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>)}
|
||||
</main>
|
||||
|
||||
{/* Passwort-Reset Dialog */}
|
||||
@@ -1060,3 +1065,95 @@ export default function AdminPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Module Tab ─────────────────────────────────────────────────────────────
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
"Planned": "bg-gray-100 text-gray-700",
|
||||
"In Progress": "bg-yellow-100 text-yellow-800",
|
||||
"In Review": "bg-blue-100 text-blue-800",
|
||||
"Deployed": "bg-green-100 text-green-800",
|
||||
};
|
||||
|
||||
const statusCounts = (list: Feature[]) => ({
|
||||
total: list.length,
|
||||
planned: list.filter((f) => f.status === "Planned").length,
|
||||
inProgress: list.filter((f) => f.status === "In Progress").length,
|
||||
inReview: list.filter((f) => f.status === "In Review").length,
|
||||
deployed: list.filter((f) => f.status === "Deployed").length,
|
||||
});
|
||||
|
||||
function ModulesTab() {
|
||||
const counts = statusCounts(features);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-lg font-semibold">Modulübersicht</h2>
|
||||
|
||||
{/* Summary bar */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
{[
|
||||
{ label: "In Progress", value: counts.inProgress, color: "bg-yellow-100 text-yellow-800" },
|
||||
{ label: "In Review", value: counts.inReview, color: "bg-blue-100 text-blue-800" },
|
||||
{ label: "Deployed", value: counts.deployed, color: "bg-green-100 text-green-800" },
|
||||
{ label: "Geplant", value: counts.planned, color: "bg-gray-100 text-gray-700" },
|
||||
].map((s) => (
|
||||
<Card key={s.label}>
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">{s.label}</span>
|
||||
<span className={`text-lg font-bold px-2 py-0.5 rounded ${s.color}`}>
|
||||
{s.value}
|
||||
</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-20">ID</TableHead>
|
||||
<TableHead>Feature</TableHead>
|
||||
<TableHead className="w-32">Status</TableHead>
|
||||
<TableHead className="w-24 text-center">Frontend</TableHead>
|
||||
<TableHead className="w-24 text-center">Backend</TableHead>
|
||||
<TableHead className="w-32">Aktualisiert</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{features.map((f) => (
|
||||
<TableRow key={f.id}>
|
||||
<TableCell className="font-mono text-xs text-muted-foreground">
|
||||
{f.id}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{f.name}</TableCell>
|
||||
<TableCell>
|
||||
<span className={`text-xs font-medium px-2 py-1 rounded-full ${statusColors[f.status]}`}>
|
||||
{f.status}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{f.frontend
|
||||
? <span className="text-green-600 font-bold">✓</span>
|
||||
: <span className="text-muted-foreground">–</span>
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{f.backend
|
||||
? <span className="text-green-600 font-bold">✓</span>
|
||||
: <span className="text-muted-foreground">–</span>
|
||||
}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{f.lastUpdated}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user