feat(admin): Kontoverwaltung und Admin-Einstellungen

"Mein Konto": Profil, Passwort setzen oder wechseln, Passkeys anlegen,
umbenennen und entfernen, aktive Sitzungen einsehen und ueberall abmelden.
Der letzte Passkey laesst sich nicht entfernen, solange kein Passwort
gesetzt ist - sonst waere das Konto ausgesperrt.

Admin-Einstellungen: Betriebsart umschalten, Selbstregistrierung freigeben,
Konten anlegen, bearbeiten, deaktivieren und loeschen. Die Instanzwerte aus
der .env werden angezeigt, damit sichtbar ist, warum Passkeys gegebenenfalls
nicht verfuegbar sind.

Schutzregeln:
- Der Wechsel in den Einzelbetrieb wird abgelehnt, solange mehr als ein
  Konto existiert.
- Der letzte aktive Administrator kann weder herabgestuft noch deaktiviert
  oder geloescht werden.
- Das eigene Konto laesst sich nicht loeschen oder deaktivieren.
- Deaktivieren oder ein neues Passwort beenden sofort alle Sitzungen des
  Kontos.

Administratoren verwalten Konten, keine Inhalte: fremde Projekte sehen auch
sie nicht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 21:30:33 +02:00
co-authored by Claude Opus 5
parent 4f85e00016
commit cf4103a46c
11 changed files with 1810 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
import type { Metadata } from "next";
import { AdminManager, type AdminData } from "@/components/admin/AdminManager";
import { requireAdminPage } from "@/lib/auth/guard";
import { getAppSettings } from "@/lib/auth/settings";
import { userSafeSelect } from "@/lib/auth/users";
import { APP_URL, WEBAUTHN_AVAILABLE } from "@/lib/config";
import { prisma } from "@/lib/db";
export const dynamic = "force-dynamic";
export async function generateMetadata(): Promise<Metadata> {
return { title: "Admin-Einstellungen" };
}
export default async function AdminPage() {
const admin = await requireAdminPage();
const [settings, users] = await Promise.all([
getAppSettings(),
prisma.user.findMany({
select: userSafeSelect,
orderBy: [{ role: "asc" }, { username: "asc" }],
}),
]);
const data: AdminData = {
mode: settings.mode,
allowRegistration: settings.allowRegistration,
users,
currentUserId: admin.id,
appUrl: APP_URL,
passkeysAvailable: WEBAUTHN_AVAILABLE,
};
return (
<div className="mx-auto max-w-4xl space-y-6 pt-4">
<header className="fade-up">
<p className="text-[11px] font-semibold uppercase tracking-[0.3em] text-violet-300/70">
Administration
</p>
<h1 className="mt-2 text-2xl font-semibold tracking-tight text-white">
Admin-Einstellungen
</h1>
<p className="mt-1 text-sm text-slate-400">
Betriebsart, Registrierung und Benutzerkonten.
</p>
</header>
<AdminManager data={data} />
</div>
);
}