Files
Moped-Tracking/src/app/(app)/admin/users/page.tsx
T

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>
</>
);
}