Rollen Chunk 7: Benutzerverwaltung und Sicherung strikt Admin-only
This commit is contained in:
1 parent
03dbf30acf
commit
c08fe7a64f
1 file changed
+247
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user