Rollen Chunk 7: Benutzerverwaltung und Sicherung strikt Admin-only
Docker Image / test (push) Canceled after 0s
Docker Image / image (push) Canceled after 0s

This commit is contained in:
loki committed 2026-10-11 20:35:56 +02:00
1 parent 03dbf30acf
commit c08fe7a64f
1 file changed
+247
+247
View File
@@ -0,0 +1,247 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import {
createUser,
deleteUser,
listUsers,
setUserRole,
type ManagedUser,
} from "@/lib/api-client";
const ROLES = ["benutzer", "teamleiter", "admin"] as const;
export function UsersTab({ selfId }: { selfId: string }) {
const [users, setUsers] = useState<ManagedUser[] | null>(null);
const [failed, setFailed] = useState(false);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [role, setRole] = useState<(typeof ROLES)[number]>("benutzer");
const [busy, setBusy] = useState(false);
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const reload = useCallback(async () => {
try {
const data = await listUsers();
setUsers(data.users);
setFailed(false);
} catch {
setFailed(true);
}
}, []);
useEffect(() => {
reload();
}, [reload]);
async function submit(event: React.FormEvent) {
event.preventDefault();
setBusy(true);
setMessage(null);
setError(null);
try {
await createUser({
name: name.trim(),
email: email.trim(),
password,
role,
});
setName("");
setEmail("");
setPassword("");
setRole("benutzer");
setMessage("Benutzer angelegt.");
reload();
} catch (createError) {
setError(
createError instanceof Error
? createError.message
: "Anlegen fehlgeschlagen.",
);
} finally {
setBusy(false);
}
}
async function changeRole(id: string, next: string) {
setMessage(null);
setError(null);
try {
await setUserRole(id, next as (typeof ROLES)[number]);
setMessage("Rolle geändert.");
reload();
} catch (roleError) {
setError(
roleError instanceof Error ? roleError.message : "Ändern fehlgeschlagen.",
);
}
}
async function remove(id: string) {
setMessage(null);
setError(null);
try {
await deleteUser(id);
setConfirmDelete(null);
setMessage("Benutzer gelöscht.");
reload();
} catch (removeError) {
setError(
removeError instanceof Error
? removeError.message
: "Löschen fehlgeschlagen.",
);
}
}
if (failed) {
return (
<section aria-label="Benutzer">
<h2>Benutzer</h2>
<p role="alert">Benutzer konnten nicht geladen werden.</p>
</section>
);
}
if (!users) {
return (
<section aria-label="Benutzer">
<h2>Benutzer</h2>
<p>Wird geladen …</p>
</section>
);
}
return (
<section aria-label="Benutzer">
<h2>Benutzer</h2>
<table>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">E-Mail</th>
<th scope="col">Rolle</th>
<th scope="col">Aktion</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<th scope="row">{user.name}</th>
<td>{user.email}</td>
<td>
<label>
Rolle
<select
value={user.role}
disabled={user.id === selfId}
onChange={(event) => changeRole(user.id, event.target.value)}
>
{ROLES.map((candidate) => (
<option key={candidate} value={candidate}>
{candidate}
</option>
))}
</select>
</label>
</td>
<td>
{user.id !== selfId && confirmDelete !== user.id && (
<button
type="button"
className="btn-secondary"
aria-label={`${user.name} löschen`}
onClick={() => setConfirmDelete(user.id)}
>
Löschen
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
{confirmDelete && (
<div className="confirm-row">
<p>Wirklich löschen? Alle Zeitdaten dieses Kontos entfallen.</p>
<div>
<button
type="button"
className="btn-danger"
onClick={() => remove(confirmDelete)}
>
Ja, löschen
</button>
<button
type="button"
className="btn-secondary"
onClick={() => setConfirmDelete(null)}
>
Abbrechen
</button>
</div>
</div>
)}
<form onSubmit={submit} aria-label="Benutzer anlegen">
<h3>Benutzer anlegen</h3>
<label>
Name
<input
type="text"
autoComplete="off"
value={name}
onChange={(event) => setName(event.target.value)}
required
/>
</label>
<label>
E-Mail
<input
type="email"
autoComplete="off"
value={email}
onChange={(event) => setEmail(event.target.value)}
required
/>
</label>
<label>
Passwort
<input
type="password"
autoComplete="new-password"
value={password}
onChange={(event) => setPassword(event.target.value)}
required
minLength={8}
/>
</label>
<label>
Rolle
<select
value={role}
onChange={(event) =>
setRole(event.target.value as (typeof ROLES)[number])
}
>
{ROLES.map((candidate) => (
<option key={candidate} value={candidate}>
{candidate}
</option>
))}
</select>
</label>
<button type="submit" className="btn-primary" disabled={busy}>
Benutzer anlegen
</button>
</form>
{message && <p className="form-message">{message}</p>}
{error && (
<p role="alert" className="form-error">
{error}
</p>
)}
</section>
);
}