Add user authentication and role management

This commit is contained in:
巫凤萍
2026-08-07 11:06:20 +08:00
parent 1f910c975d
commit c926a6a874
25 changed files with 2370 additions and 132 deletions

212
app/users-page.tsx Normal file
View File

@@ -0,0 +1,212 @@
"use client";
import { FormEvent, useCallback, useEffect, useState } from "react";
import type { AuthUser, UserRole } from "../lib/user-auth";
type ManagedUser = {
id: string;
username: string;
role: UserRole;
created_at: string;
updated_at: string;
};
const ROLE_LABEL: Record<UserRole, string> = {
super_admin: "超级管理员",
admin: "管理员",
user: "普通用户",
};
async function userApi(response: Response) {
const result = (await response.json()) as { users?: ManagedUser[]; error?: string };
if (!response.ok) throw new Error(result.error || "操作失败");
return result.users ?? [];
}
export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
const [users, setUsers] = useState<ManagedUser[]>([]);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [role, setRole] = useState<"admin" | "user">("user");
const [resetTarget, setResetTarget] = useState<ManagedUser | null>(null);
const [resetPassword, setResetPassword] = useState("");
const [message, setMessage] = useState("");
const [working, setWorking] = useState(false);
const loadUsers = useCallback(async () => {
try {
setUsers(await userApi(await fetch("/api/users", { cache: "no-store" })));
} catch (reason) {
setMessage(reason instanceof Error ? reason.message : "加载失败");
}
}, []);
useEffect(() => {
const timer = window.setTimeout(() => {
void loadUsers();
}, 0);
return () => window.clearTimeout(timer);
}, [loadUsers]);
const createUser = async (event: FormEvent) => {
event.preventDefault();
setWorking(true);
setMessage("");
try {
const next = await userApi(
await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "create", username, password, role }),
}),
);
setUsers(next);
setUsername("");
setPassword("");
setRole("user");
setMessage("账号已创建,可以把账号和密码交给用户登录");
} catch (reason) {
setMessage(reason instanceof Error ? reason.message : "创建失败");
} finally {
setWorking(false);
}
};
const resetUserPassword = async (event: FormEvent) => {
event.preventDefault();
if (!resetTarget) return;
setWorking(true);
setMessage("");
try {
const next = await userApi(
await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "reset_password",
userId: resetTarget.id,
password: resetPassword,
}),
}),
);
setUsers(next);
setResetTarget(null);
setResetPassword("");
setMessage("密码已重置,旧登录状态已退出");
} catch (reason) {
setMessage(reason instanceof Error ? reason.message : "重置失败");
} finally {
setWorking(false);
}
};
const canReset = (item: ManagedUser) => {
if (currentUser.role === "super_admin") {
return item.role !== "super_admin" || item.id === currentUser.id;
}
return item.role === "user";
};
return (
<div className="user-management-layout">
<section className="panel user-create-panel">
<div className="panel-heading">
<div>
<h2></h2>
<p></p>
</div>
</div>
<form className="user-create-form" onSubmit={createUser}>
<label>
<span></span>
<input
value={username}
onChange={(event) => setUsername(event.target.value)}
placeholder="例如linlin01"
autoComplete="off"
required
/>
</label>
<label>
<span></span>
<input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="至少8位"
autoComplete="new-password"
minLength={8}
required
/>
</label>
{currentUser.role === "super_admin" && (
<label>
<span></span>
<select value={role} onChange={(event) => setRole(event.target.value as "admin" | "user")}>
<option value="user"></option>
<option value="admin"></option>
</select>
</label>
)}
<button className="primary-button" disabled={working}>
{working ? "处理中…" : "创建账号"}
</button>
</form>
{message && <p className="user-management-message">{message}</p>}
</section>
<section className="panel user-list-panel">
<div className="panel-heading">
<div>
<h2></h2>
<p> {users.length} </p>
</div>
</div>
<div className="user-table">
<div className="user-table-head">
<span></span><span></span><span></span><span></span>
</div>
{users.map((item) => (
<div className="user-table-row" key={item.id}>
<strong>{item.username}</strong>
<span className={`role-pill ${item.role}`}>{ROLE_LABEL[item.role]}</span>
<span>{new Date(item.updated_at).toLocaleDateString("zh-CN", { timeZone: "Asia/Shanghai" })}</span>
{canReset(item) ? (
<button className="text-button" onClick={() => { setResetTarget(item); setResetPassword(""); }}>
</button>
) : <span></span>}
</div>
))}
</div>
</section>
{resetTarget && (
<div className="modal-backdrop" role="presentation" onMouseDown={() => setResetTarget(null)}>
<form className="modal-card compact" onSubmit={resetUserPassword} onMouseDown={(event) => event.stopPropagation()}>
<div className="modal-heading">
<div><p className="eyebrow"></p><h2> {resetTarget.username} </h2></div>
<button type="button" onClick={() => setResetTarget(null)} aria-label="关闭">×</button>
</div>
<label>
<span></span>
<input
type="password"
value={resetPassword}
onChange={(event) => setResetPassword(event.target.value)}
minLength={8}
autoComplete="new-password"
required
/>
<small>退</small>
</label>
<div className="modal-actions">
<button type="button" className="ghost-button" onClick={() => setResetTarget(null)}></button>
<button className="primary-button" disabled={working}></button>
</div>
</form>
</div>
)}
</div>
);
}