Drei Zustaende in der Kopfzeile — System, Hell, Dunkel. Drei statt zwei, weil ein reiner Ein/Aus-Schalter nach dem ersten Klick fuer immer eine feste Wahl bedeutet; wer "System" waehlt, bekommt abends dunkel und morgens hell. Umgesetzt ueber `light-dark()` im CSS, nicht ueber `dark:`-Klassen. Die Oberflaeche benutzt an rund 440 Stellen feste Farbklassen; jede davon zu verdoppeln waere eine riesige Fleissarbeit, und jede kuenftige Zeile muesste daran denken. Stattdessen entscheiden die Farbvariablen selbst, welchen Wert sie tragen — gesteuert ueber `color-scheme`. Deshalb genuegt fuer den Umschalter ein Attribut am html-Element, und die Systemwahl funktioniert sogar ohne JavaScript. Das Skript im Kopf setzt nur eine ausdrueckliche Wahl, damit die helle Fassung nicht kurz aufblitzt. Zwei Faelle liessen sich nicht ueber Variablen loesen: `bg-white` muss im Dunkeln eine dunkle Flaeche sein, `text-white` dagegen weiss bleiben — beide teilen sich aber dieselbe Variable. Fuer Flaechen gibt es deshalb jetzt `bg-surface`. Ebenso brauchen Akzent-Links im Dunkeln eine hellere Stufe als Akzent-Knoepfe: dafuer `text-accent-text` und `bg-accent-soft`. Bewusst unveraendert bleiben die QR-Codes selbst — deren Farben legt die Gestaltung fest. Die dunklen Werte sind keine Spiegelung: Reines Schwarz unter weisser Schrift flimmert. Alle Textkontraste sind gemessen und liegen ueber 4,5:1, der niedrigste bei 5,9:1.
154 lines
5.5 KiB
TypeScript
154 lines
5.5 KiB
TypeScript
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
|
|
import { ChangePasswordForm, SignOutCard } from "@/components/account";
|
|
import { Badge, Card, CardTitle, Notice } from "@/components/ui";
|
|
import { isAuthEnabled } from "@/lib/auth/actor";
|
|
import { requirePageActor } from "@/lib/auth/guard";
|
|
import { MIN_PASSWORD_LENGTH } from "@/lib/auth/password";
|
|
import { ROLE_DESCRIPTIONS, ROLE_LABELS } from "@/lib/auth/policy";
|
|
import { prisma } from "@/lib/db";
|
|
|
|
export const metadata: Metadata = { title: "Konto" };
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function AccountPage() {
|
|
const actor = await requirePageActor();
|
|
|
|
// Konten aus dem externen Verzeichnis haben hier kein Passwort.
|
|
const ausVerzeichnis =
|
|
actor.kind === "user"
|
|
? ((await prisma.user.findUnique({
|
|
where: { id: actor.user.id },
|
|
select: { passwordHash: true },
|
|
}))?.passwordHash ?? null) === null
|
|
: false;
|
|
|
|
// Bei ausgeschalteter Anmeldung gibt es kein Konto — dann erklärt die Seite,
|
|
// wie sich das umstellen lässt, statt einfach leer zu sein.
|
|
if (actor.kind !== "user") {
|
|
return (
|
|
<div className="mx-auto max-w-2xl px-4 py-10 sm:px-6">
|
|
<h1 className="text-2xl font-bold tracking-tight text-slate-900">
|
|
Konto
|
|
</h1>
|
|
<div className="mt-5">
|
|
<Notice tone="info">
|
|
<span className="font-medium">Die Anmeldung ist ausgeschaltet.</span>{" "}
|
|
Es gibt deshalb kein Konto und niemanden zum Abmelden — wer die
|
|
Anwendung erreicht, darf alles. Zum Einschalten{" "}
|
|
<code className="rounded bg-surface/70 px-1.5 py-0.5 font-mono text-xs">
|
|
AUTH_ENABLED=true
|
|
</code>{" "}
|
|
in der <code className="font-mono text-xs">.env</code> setzen und{" "}
|
|
<code className="rounded bg-surface/70 px-1.5 py-0.5 font-mono text-xs">
|
|
docker compose up -d
|
|
</code>{" "}
|
|
ausführen. Beim nächsten Aufruf legst du den ersten Administrator
|
|
an.
|
|
</Notice>
|
|
</div>
|
|
<Link
|
|
href="/"
|
|
className="mt-6 inline-flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
|
|
>
|
|
Zur Typ-Auswahl
|
|
</Link>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const { user } = actor;
|
|
|
|
return (
|
|
<div className="mx-auto max-w-2xl px-4 py-8 sm:px-6 sm:py-10">
|
|
<div className="mb-7">
|
|
<h1 className="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">
|
|
Konto
|
|
</h1>
|
|
<p className="mt-2 text-slate-600">
|
|
Deine Anmeldedaten und die Gruppen, in denen du mitarbeitest.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<Card>
|
|
<CardTitle>Angemeldet als</CardTitle>
|
|
<dl className="space-y-2 text-sm">
|
|
<div className="flex flex-wrap gap-x-2">
|
|
<dt className="text-slate-500">Name:</dt>
|
|
<dd className="text-slate-900">{user.name?.trim() || "—"}</dd>
|
|
</div>
|
|
<div className="flex flex-wrap gap-x-2">
|
|
<dt className="text-slate-500">E-Mail:</dt>
|
|
<dd className="text-slate-900">{user.email}</dd>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-x-2">
|
|
<dt className="text-slate-500">Rolle:</dt>
|
|
<dd>
|
|
{user.isInstanceAdmin ? (
|
|
<Badge tone="accent">Administrator der Installation</Badge>
|
|
) : (
|
|
<span className="text-slate-900">Benutzer</span>
|
|
)}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardTitle
|
|
hint={
|
|
user.isInstanceAdmin
|
|
? "Als Administrator der Installation siehst du ohnehin alle Gruppen."
|
|
: undefined
|
|
}
|
|
>
|
|
Meine Gruppen
|
|
</CardTitle>
|
|
{user.memberships.length === 0 ? (
|
|
<p className="text-sm text-slate-600">
|
|
Du bist in keiner Gruppe. Bitte einen Administrator, dich einer
|
|
zuzuordnen — sonst kannst du keine Codes anlegen.
|
|
</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{user.memberships.map((membership) => (
|
|
<li
|
|
key={membership.groupId}
|
|
className="flex flex-wrap items-center gap-2 rounded-lg border border-slate-200 p-3"
|
|
>
|
|
<span className="font-medium text-slate-900">
|
|
{membership.groupName}
|
|
</span>
|
|
<Badge tone="neutral">{ROLE_LABELS[membership.role]}</Badge>
|
|
<span className="w-full text-sm text-slate-500">
|
|
{ROLE_DESCRIPTIONS[membership.role]}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</Card>
|
|
|
|
{isAuthEnabled() ? (
|
|
<>
|
|
{ausVerzeichnis ? (
|
|
<Card>
|
|
<CardTitle>Passwort</CardTitle>
|
|
<p className="mt-2 text-sm text-slate-600">
|
|
Dieses Konto meldet sich über den externen Anbieter an. Ein
|
|
Passwort gibt es hier nicht — geändert wird es beim Anbieter.
|
|
</p>
|
|
</Card>
|
|
) : (
|
|
<ChangePasswordForm minLength={MIN_PASSWORD_LENGTH} />
|
|
)}
|
|
<SignOutCard />
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|