Files
Speisekarte/components/UserManager.tsx
T
florian.wilde 00745ae48b Benutzerverwaltung: Passwort-Wiederholung gegen Tippfehler
Gilt beim Anlegen (Pflicht) und beim optionalen Passwort-Reset im
Bearbeiten-Formular.
2026-07-18 00:46:39 +02:00

270 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import type { Profile, Role } from "@/lib/types";
interface UserForm {
id: string | null; // null = neuer Benutzer
email: string;
full_name: string;
role: Role;
password: string; // bei Bearbeitung optional (leer = unverändert)
passwordRepeat: string;
}
const EMPTY_FORM: UserForm = {
id: null,
email: "",
full_name: "",
role: "staff",
password: "",
passwordRepeat: "",
};
const ROLE_LABELS: Record<Role, string> = {
admin: "Administrator",
staff: "Kantinenmitarbeiter",
};
export default function UserManager() {
const supabase = useMemo(() => createClient(), []);
const [profiles, setProfiles] = useState<Profile[]>([]);
const [me, setMe] = useState<string | null>(null);
const [form, setForm] = useState<UserForm | null>(null);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
const [{ data: profileData, error: profileError }, { data: userData }] =
await Promise.all([
supabase.from("profiles").select("*").order("created_at"),
supabase.auth.getUser(),
]);
if (profileError) setError(`Laden fehlgeschlagen: ${profileError.message}`);
else setProfiles(profileData as Profile[]);
setMe(userData.user?.id ?? null);
}, [supabase]);
useEffect(() => {
load();
}, [load]);
async function callApi(method: string, body: unknown): Promise<boolean> {
setBusy(true);
setError(null);
try {
const res = await fetch("/api/admin/users", {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const data = await res.json().catch(() => null);
setError(data?.error ?? `Fehler (${res.status})`);
return false;
}
return true;
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
return false;
} finally {
setBusy(false);
}
}
async function save(e: React.FormEvent) {
e.preventDefault();
if (!form) return;
if (form.password && form.password !== form.passwordRepeat) {
setError("Die Passwörter stimmen nicht überein.");
return;
}
const ok = form.id
? await callApi("PATCH", {
id: form.id,
full_name: form.full_name,
role: form.role,
password: form.password || undefined,
})
: await callApi("POST", {
email: form.email,
full_name: form.full_name,
role: form.role,
password: form.password,
});
if (ok) {
setForm(null);
await load();
}
}
async function remove(profile: Profile) {
if (
!window.confirm(
`Benutzer „${profile.email}“ wirklich löschen? Der Zugang wird dauerhaft entfernt.`
)
)
return;
if (await callApi("DELETE", { id: profile.id })) await load();
}
return (
<div className="max-w-3xl">
<div className="flex items-center gap-3 mb-4">
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
Benutzer
</h1>
<button
type="button"
className="btn btn-add"
onClick={() => setForm(EMPTY_FORM)}
>
Neuer Benutzer
</button>
</div>
{error && (
<p className="mb-3 text-sm text-[var(--c-del)]" role="alert">
{error}
</p>
)}
{form && (
<form onSubmit={save} className="card mb-4 space-y-3">
<h2 className="font-semibold">
{form.id ? "Benutzer bearbeiten" : "Neuer Benutzer"}
</h2>
{!form.id && (
<div>
<label className="label" htmlFor="user-email">
E-Mail-Adresse
</label>
<input
id="user-email"
type="email"
className="input"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
required
/>
</div>
)}
<div>
<label className="label" htmlFor="user-name">
Name
</label>
<input
id="user-name"
className="input"
value={form.full_name}
onChange={(e) => setForm({ ...form, full_name: e.target.value })}
/>
</div>
<div className="max-w-60">
<label className="label" htmlFor="user-role">
Rolle
</label>
<select
id="user-role"
className="input"
value={form.role}
onChange={(e) =>
setForm({ ...form, role: e.target.value as Role })
}
>
<option value="staff">Kantinenmitarbeiter</option>
<option value="admin">Administrator</option>
</select>
</div>
<div className="max-w-60">
<label className="label" htmlFor="user-password">
{form.id ? "Neues Passwort (optional)" : "Passwort"}
</label>
<input
id="user-password"
type="password"
className="input"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
minLength={8}
required={!form.id}
autoComplete="new-password"
/>
</div>
{(!form.id || form.password) && (
<div className="max-w-60">
<label className="label" htmlFor="user-password-repeat">
Passwort wiederholen
</label>
<input
id="user-password-repeat"
type="password"
className="input"
value={form.passwordRepeat}
onChange={(e) =>
setForm({ ...form, passwordRepeat: e.target.value })
}
minLength={8}
required={!form.id || !!form.password}
autoComplete="new-password"
/>
</div>
)}
<div className="flex gap-2">
<button type="submit" className="btn btn-add" disabled={busy}>
Speichern
</button>
<button
type="button"
className="btn btn-neutral"
onClick={() => setForm(null)}
>
Abbrechen
</button>
</div>
</form>
)}
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-md bg-white">
{profiles.map((p) => (
<li key={p.id} className="flex items-center gap-3 px-4 py-2 text-sm">
<span className="mr-auto">
<span className="font-semibold">{p.full_name || p.email}</span>{" "}
<span className="text-gray-500">
– {p.email} – {ROLE_LABELS[p.role]}
</span>
{p.id === me && <span className="text-gray-400"> (du)</span>}
</span>
<button
type="button"
className="btn btn-primary"
onClick={() =>
setForm({
id: p.id,
email: p.email,
full_name: p.full_name,
role: p.role,
password: "",
passwordRepeat: "",
})
}
>
Bearbeiten
</button>
{p.id !== me && (
<button
type="button"
className="btn btn-del"
onClick={() => remove(p)}
>
Löschen
</button>
)}
</li>
))}
</ul>
</div>
);
}