feat: resolve all 22 items in Features and Bugs.txt and complete UI polish
This commit is contained in:
+37
-14
@@ -2,7 +2,7 @@ import type { FastifyInstance } from "fastify";
|
||||
import { ulid } from "ulid";
|
||||
import { z } from "zod";
|
||||
import { requireAdmin, hashPassword } from "./auth.js";
|
||||
import { auditLog } from "./db.js";
|
||||
import { auditLog, listUsers } from "./db.js";
|
||||
import { toUser, toAudit } from "./mapping.js";
|
||||
import type { UserRow, AuditRow } from "./mapping.js";
|
||||
|
||||
@@ -38,6 +38,10 @@ export function registerAdminRoutes(app: FastifyInstance): void {
|
||||
|
||||
// ---- users ----
|
||||
|
||||
instance.get("/api/admin/users", async () => {
|
||||
return listUsers(db);
|
||||
});
|
||||
|
||||
instance.post("/api/admin/users", async (req) => {
|
||||
const body = userCreate.parse(req.body);
|
||||
const existing = db
|
||||
@@ -47,12 +51,12 @@ export function registerAdminRoutes(app: FastifyInstance): void {
|
||||
|
||||
const id = ulid();
|
||||
const role = body.role ?? "author";
|
||||
const passHash = await hashPassword(body.password);
|
||||
const passHash = await hashPassword(body.password ?? "changeme123");
|
||||
db.prepare(
|
||||
"INSERT INTO users (id, email, pass_hash, display_name, role, disabled, created_at) VALUES (?, ?, ?, ?, ?, 0, ?)",
|
||||
).run(id, body.email, passHash, body.displayName, role, new Date().toISOString());
|
||||
|
||||
auditLog(db, "user_create", req.user!.id, { email: body.email, role });
|
||||
auditLog(db, "user_create", req.user!.id, { targetId: id, email: body.email, role });
|
||||
|
||||
const row = db.prepare("SELECT * FROM users WHERE id = ?").get(id) as UserRow;
|
||||
return toUser(row);
|
||||
@@ -64,10 +68,6 @@ export function registerAdminRoutes(app: FastifyInstance): void {
|
||||
const row = db.prepare("SELECT * FROM users WHERE id = ?").get(id) as UserRow | undefined;
|
||||
if (!row) throw instance.httpErrors.create(404, { error: "user_not_found" });
|
||||
|
||||
if (row.disabled && !body.disabled) {
|
||||
throw instance.httpErrors.create(400, { error: "reset_password_to_reenable" });
|
||||
}
|
||||
|
||||
const fields: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
if (body.displayName !== undefined) {
|
||||
@@ -81,10 +81,18 @@ export function registerAdminRoutes(app: FastifyInstance): void {
|
||||
if (body.disabled !== undefined) {
|
||||
fields.push("disabled = ?");
|
||||
values.push(body.disabled ? 1 : 0);
|
||||
if (body.disabled) {
|
||||
db.prepare("DELETE FROM sessions WHERE user_id = ?").run(id);
|
||||
}
|
||||
}
|
||||
if (body.password !== undefined) {
|
||||
const passHash = await hashPassword(body.password);
|
||||
fields.push("pass_hash = ?");
|
||||
values.push(passHash);
|
||||
}
|
||||
if (fields.length) db.prepare(`UPDATE users SET ${fields.join(", ")} WHERE id = ?`).run(...values, id);
|
||||
|
||||
auditLog(db, "user_update", req.user!.id, { id, changes: Object.keys(body) });
|
||||
auditLog(db, "user_update", req.user!.id, { targetId: id, changes: Object.keys(body) });
|
||||
|
||||
const updated = db.prepare("SELECT * FROM users WHERE id = ?").get(id) as UserRow;
|
||||
return toUser(updated);
|
||||
@@ -150,9 +158,23 @@ export function registerAdminRoutes(app: FastifyInstance): void {
|
||||
const users = (db.prepare("SELECT COUNT(*) AS n FROM users").get() as { n: number }).n;
|
||||
const disabled = (db.prepare("SELECT COUNT(*) AS n FROM users WHERE disabled = 1").get() as { n: number }).n;
|
||||
const projects = (db.prepare("SELECT COUNT(*) AS n FROM projects").get() as { n: number }).n;
|
||||
const chapters = (db.prepare("SELECT COUNT(*) AS n FROM chapters").get() as { n: number }).n;
|
||||
const worldItems = (db.prepare("SELECT COUNT(*) AS n FROM entities WHERE kind = 'world'").get() as {
|
||||
n: number;
|
||||
}).n;
|
||||
const shares = (db.prepare("SELECT COUNT(*) AS n FROM project_shares").get() as { n: number }).n;
|
||||
const audit = (db.prepare("SELECT COUNT(*) AS n FROM audit").get() as { n: number }).n;
|
||||
return { users, disabled, projects, shares, audit };
|
||||
return {
|
||||
users,
|
||||
disabled,
|
||||
disabledUsers: disabled,
|
||||
projects,
|
||||
chapters,
|
||||
worldItems,
|
||||
shares,
|
||||
audit,
|
||||
auditEntries: audit,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -161,13 +183,14 @@ export function registerAdminRoutes(app: FastifyInstance): void {
|
||||
|
||||
const userCreate = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
password: z.string().min(6).optional(),
|
||||
displayName: z.string().min(1).max(80),
|
||||
role: z.enum(["admin", "author"]),
|
||||
role: z.enum(["admin", "author"]).optional(),
|
||||
});
|
||||
|
||||
const userPatch = z.object({
|
||||
role: z.enum(["admin", "author"]),
|
||||
disabled: z.boolean(),
|
||||
displayName: z.string().min(1).max(80),
|
||||
role: z.enum(["admin", "author"]).optional(),
|
||||
disabled: z.boolean().optional(),
|
||||
displayName: z.string().min(1).max(80).optional(),
|
||||
password: z.string().min(6).optional(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user