返回 DeepSeek-Reasonix
admin.ts
根目录 / workers / crash-report / src / admin.ts
1 import { esc, page } from "./shell";
2 import { type Role, type User, userNav } from "./auth";
3
4 export interface UserRow {
5 id: number;
6 email: string;
7 role: Role;
8 created_at: string;
9 approved_at: string | null;
10 }
11
12 export interface AuditRow {
13 at: string;
14 actor_email: string;
15 action: string;
16 target: string;
17 detail: string;
18 }
19
20 function roleSelect(row: UserRow): string {
21 const opt = (r: Role) => `<option value="${r}"${row.role === r ? " selected" : ""}>${r}</option>`;
22 return `<form method="post" action="/admin/users" class="actions">
23 <input type="hidden" name="action" value="role"><input type="hidden" name="userId" value="${row.id}">
24 <select name="role" onchange="this.form.submit()">${opt("pending")}${opt("viewer")}${opt("admin")}</select>
25 <noscript><button class="btn sm" type="submit">Set</button></noscript></form>`;
26 }
27
28 function deleteForm(row: UserRow): string {
29 return `<form method="post" action="/admin/users" class="inline" onsubmit="return confirm('Delete ${esc(row.email)}?')">
30 <input type="hidden" name="action" value="delete"><input type="hidden" name="userId" value="${row.id}">
31 <button class="btn danger sm" type="submit">Delete</button></form>`;
32 }
33
34 export function renderUsers(viewer: User, users: UserRow[], n?: { kind: "err" | "ok"; text: string }): string {
35 const pending = users.filter((u) => u.role === "pending").length;
36 const rows = users
37 .map((u) => {
38 const self = u.id === viewer.id;
39 const approved = u.approved_at ? esc(u.approved_at.slice(0, 10)) : "—";
40 return `<tr><td>${esc(u.email)}${self ? ' <span class="badge viewer">you</span>' : ""}</td>
41 <td>${self ? `<span class="badge ${u.role}">${u.role}</span>` : roleSelect(u)}</td>
42 <td class="n">${esc(u.created_at.slice(0, 10))}</td><td class="n">${approved}</td>
43 <td>${self ? "" : deleteForm(u)}</td></tr>`;
44 })
45 .join("");
46 return page(
47 "Reasonix · Users",
48 "admin / users",
49 `<h1>Users</h1><p class="sub"><b>${users.length}</b> accounts · <b>${pending}</b> awaiting approval · set a role to grant or revoke access</p>
50 ${n ? `<div class="notice ${n.kind}">${esc(n.text)}</div>` : ""}
51 <div class="card full"><table><thead><tr><th>email</th><th>role</th><th>joined</th><th>approved</th><th></th></tr></thead>
52 <tbody>${rows}</tbody></table></div>
53 <a class="back" href="/admin/audit">View audit log →</a>`,
54 userNav(viewer),
55 );
56 }
57
58 export function renderAudit(viewer: User, rows: AuditRow[]): string {
59 const body = rows.length
60 ? `<table><thead><tr><th>when</th><th>actor</th><th>action</th><th>target</th><th>detail</th></tr></thead><tbody>${rows
61 .map(
62 (r) =>
63 `<tr><td class="n">${esc(r.at.slice(0, 19).replace("T", " "))}</td><td>${esc(r.actor_email)}</td><td><span class="pill">${esc(r.action)}</span></td><td>${esc(r.target)}</td><td>${esc(r.detail)}</td></tr>`,
64 )
65 .join("")}</tbody></table>`
66 : `<div class="empty">No actions logged yet</div>`;
67 return page(
68 "Reasonix · Audit",
69 "admin / audit",
70 `<h1>Audit log</h1><p class="sub">Permission and report-data changes, newest first</p>
71 <div class="card full">${body}</div>
72 <a class="back" href="/admin">← Back to users</a>`,
73 userNav(viewer),
74 );
75 }
76
76 lines TYPESCRIPT