154 lines
5.2 KiB
TypeScript
154 lines
5.2 KiB
TypeScript
import Link from "next/link";
|
|
import {
|
|
changeUserRoleAction,
|
|
createUserAction,
|
|
deleteUserAction,
|
|
} from "@/app/actions/users";
|
|
import { Flash } from "@/components/flash";
|
|
import { ResetUserPassword } from "@/components/reset-user-password";
|
|
import { requireAdminUser } from "@/lib/auth";
|
|
import { db, initDatabase } from "@/lib/db";
|
|
import { formatDate, queryMessage } from "@/lib/format";
|
|
import type { UserRole } from "@/lib/types";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
type UserRow = {
|
|
id: number;
|
|
name: string;
|
|
role: UserRole;
|
|
created_at: string;
|
|
};
|
|
|
|
export default async function AdminUsersPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
}) {
|
|
const currentUser = await requireAdminUser();
|
|
initDatabase();
|
|
const params = await searchParams;
|
|
|
|
const users = db
|
|
.prepare(
|
|
"SELECT id, name, role, created_at FROM users WHERE active = 1 ORDER BY name COLLATE NOCASE",
|
|
)
|
|
.all() as UserRow[];
|
|
const adminCount = users.filter((u) => u.role === "admin").length;
|
|
const currentAdminKeyCount = db
|
|
.prepare("SELECT COUNT(*) AS count FROM webauthn_credentials WHERE user_id = ?")
|
|
.get(currentUser.id) as { count: number };
|
|
|
|
return (
|
|
<>
|
|
<div className="breadcrumbs">
|
|
<Link href="/">Übersicht</Link>
|
|
<span>/</span>
|
|
Benutzer
|
|
</div>
|
|
<div className="page-heading">
|
|
<div>
|
|
<p className="eyebrow">Administration</p>
|
|
<h1>Benutzerverwaltung</h1>
|
|
<p className="muted">{users.length} Konten · {adminCount} Administrator(en)</p>
|
|
</div>
|
|
</div>
|
|
<Flash error={queryMessage(params, "error")} success={queryMessage(params, "success")} />
|
|
|
|
<section className="section-block">
|
|
<div className="section-heading">
|
|
<div>
|
|
<p className="eyebrow">Team</p>
|
|
<h2>Konten</h2>
|
|
</div>
|
|
</div>
|
|
<div className="panel table-wrap">
|
|
<table className="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Rolle</th>
|
|
<th>Angelegt</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.map((row) => {
|
|
const lastAdmin = row.role === "admin" && adminCount <= 1;
|
|
const isSelf = row.id === currentUser.id;
|
|
return (
|
|
<tr key={row.id}>
|
|
<td>{row.name}{isSelf && <span className="badge">Du</span>}</td>
|
|
<td>
|
|
<form action={changeUserRoleAction} className="inline-form">
|
|
<input type="hidden" name="id" value={row.id} />
|
|
<select
|
|
name="role"
|
|
defaultValue={row.role}
|
|
disabled={lastAdmin || isSelf}
|
|
>
|
|
<option value="member">Mitglied</option>
|
|
<option value="admin">Administrator</option>
|
|
</select>
|
|
<button className="link-button" type="submit" disabled={lastAdmin || isSelf}>
|
|
Speichern
|
|
</button>
|
|
</form>
|
|
</td>
|
|
<td>{formatDate(row.created_at.slice(0, 10))}</td>
|
|
<td>
|
|
{!isSelf && (
|
|
<div className="user-admin-actions">
|
|
<ResetUserPassword
|
|
userId={row.id}
|
|
requiresYubiKey={currentAdminKeyCount.count > 0}
|
|
/>
|
|
{!lastAdmin && (
|
|
<form action={deleteUserAction} className="inline-form">
|
|
<input type="hidden" name="id" value={row.id} />
|
|
<button className="link-button danger" type="submit">Löschen</button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="section-block">
|
|
<div className="section-heading">
|
|
<div>
|
|
<p className="eyebrow">Neu</p>
|
|
<h2>Benutzer anlegen</h2>
|
|
</div>
|
|
</div>
|
|
<form action={createUserAction} className="panel form-grid">
|
|
<label>
|
|
Name *
|
|
<input name="name" required maxLength={100} />
|
|
</label>
|
|
<label>
|
|
Passwort * (min. 10 Zeichen)
|
|
<input name="password" type="password" required minLength={10} maxLength={200} />
|
|
</label>
|
|
<label>
|
|
Rolle *
|
|
<select name="role" defaultValue="member" required>
|
|
<option value="member">Mitglied</option>
|
|
<option value="admin">Administrator</option>
|
|
</select>
|
|
</label>
|
|
<div className="form-actions span-2">
|
|
<button className="button button-primary" type="submit">Benutzer anlegen</button>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|