feat: optimize user management and add account deletion

This commit is contained in:
巫凤萍
2026-08-07 17:08:56 +08:00
parent 5670a7939b
commit 1f1887c860
5 changed files with 329 additions and 23 deletions

View File

@@ -57,14 +57,14 @@ export async function POST(request: Request) {
role?: UserRole;
userId?: string;
};
const password = String(body.password ?? "");
if (!validatePassword(password)) {
return Response.json({ error: "密码需为8—72位" }, { status: 400 });
}
const db = getRawDb();
const passwordRecord = await createPasswordRecord(password);
if (body.action === "create") {
const password = String(body.password ?? "");
if (!validatePassword(password)) {
return Response.json({ error: "密码需为8—72位" }, { status: 400 });
}
const passwordRecord = await createPasswordRecord(password);
const username = normalizeUsername(body.username);
const role = body.role === "admin" ? "admin" : "user";
if (!validateUsername(username)) {
@@ -92,6 +92,11 @@ export async function POST(request: Request) {
)
.run();
} else if (body.action === "reset_password") {
const password = String(body.password ?? "");
if (!validatePassword(password)) {
return Response.json({ error: "密码需为8—72位" }, { status: 400 });
}
const passwordRecord = await createPasswordRecord(password);
const target = await db
.prepare("SELECT id, role FROM users WHERE id = ?")
.bind(String(body.userId ?? ""))
@@ -124,6 +129,27 @@ export async function POST(request: Request) {
),
db.prepare("DELETE FROM auth_sessions WHERE user_id = ?").bind(target.id),
]);
} else if (body.action === "delete") {
const target = await db
.prepare("SELECT id, role FROM users WHERE id = ?")
.bind(String(body.userId ?? ""))
.first<{ id: string; role: UserRole }>();
if (!target) {
return Response.json({ error: "没有找到这个账号" }, { status: 404 });
}
if (target.id === currentUser.id) {
return Response.json({ error: "不能删除当前登录账号" }, { status: 400 });
}
if (target.role === "super_admin") {
return Response.json({ error: "不能删除超级管理员账号" }, { status: 403 });
}
if (currentUser.role !== "super_admin" && target.role !== "user") {
return managerForbidden();
}
await db.batch([
db.prepare("DELETE FROM auth_sessions WHERE user_id = ?").bind(target.id),
db.prepare("DELETE FROM users WHERE id = ? AND role <> 'super_admin'").bind(target.id),
]);
} else {
return Response.json({ error: "不支持的操作" }, { status: 400 });
}

View File

@@ -2877,25 +2877,42 @@ label small {
.user-management-layout {
display: grid;
grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.28fr);
gap: 18px;
grid-template-columns: 1fr;
gap: 16px;
min-width: 0;
}
.user-create-panel,
.user-list-panel {
align-self: start;
min-width: 0;
padding: 22px;
}
.user-create-form {
display: flex;
flex-direction: column;
margin-top: 24px;
display: grid;
gap: 12px;
align-items: end;
margin-top: 18px;
}
.user-create-form.with-role {
grid-template-columns: minmax(190px, 1.15fr) minmax(190px, 1fr) minmax(150px, 0.72fr) 132px;
}
.user-create-form.without-role {
grid-template-columns: minmax(190px, 1fr) minmax(190px, 1fr) 132px;
}
.user-create-form label {
min-width: 0;
margin-bottom: 0;
}
.user-create-form .primary-button {
width: 100%;
height: 45px;
margin-top: 3px;
height: 40px;
min-height: 40px;
}
.user-management-message {
@@ -2908,7 +2925,9 @@ label small {
}
.user-table {
margin-top: 18px;
width: 100%;
max-width: 100%;
margin-top: 16px;
border: 1px solid var(--line);
border-radius: 12px;
overflow: hidden;
@@ -2917,10 +2936,10 @@ label small {
.user-table-head,
.user-table-row {
display: grid;
grid-template-columns: minmax(130px, 1.4fr) minmax(90px, 0.8fr) minmax(90px, 0.8fr) 82px;
grid-template-columns: minmax(160px, 1.4fr) minmax(100px, 0.7fr) minmax(110px, 0.75fr) minmax(180px, auto);
gap: 12px;
align-items: center;
padding: 12px 14px;
padding: 14px 16px;
}
.user-table-head {
@@ -2934,6 +2953,10 @@ label small {
font-size: 10px;
}
.user-table-row:hover {
background: #fbfcfa;
}
.user-table-row > span:not(.role-pill) {
color: #84908c;
}
@@ -2958,6 +2981,77 @@ label small {
background: #e5f5ed;
}
.user-row-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
}
.user-action-button {
min-height: 30px;
padding: 0 10px;
border: 1px solid #dce5e1;
border-radius: 8px;
color: var(--green-deep);
background: white;
font-size: 9px;
font-weight: 650;
}
.user-action-button:hover {
border-color: #a8cbbd;
background: #f4faf7;
}
.user-action-button.danger {
border-color: #efd7d3;
color: #a8473b;
}
.user-action-button.danger:hover {
border-color: #dfb5ae;
background: #fff7f5;
}
.danger-button {
display: inline-flex;
min-height: 38px;
align-items: center;
justify-content: center;
padding: 0 15px;
border: 1px solid #a83f34;
border-radius: 9px;
color: white;
background: #b94d41;
font-size: 11px;
font-weight: 650;
}
.danger-button:hover {
background: #9f3e34;
}
.danger-button:disabled {
cursor: wait;
opacity: 0.62;
}
.eyebrow.danger {
color: #b04b40;
}
.user-delete-warning {
margin: 0;
padding: 13px 14px;
border: 1px solid #f0dedb;
border-radius: 10px;
color: #7f5049;
background: #fff8f6;
font-size: 10px;
line-height: 1.65;
}
.toast {
position: fixed;
right: 24px;
@@ -3101,6 +3195,15 @@ label small {
.schedule-grid {
grid-template-columns: 1fr;
}
.user-create-form.with-role,
.user-create-form.without-role {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.user-create-form .primary-button {
align-self: end;
}
}
@media (max-width: 900px) {
@@ -3216,7 +3319,17 @@ label small {
.user-table-head,
.user-table-row {
min-width: 560px;
min-width: 660px;
}
.user-create-panel,
.user-list-panel {
padding: 18px;
}
.user-create-form.with-role,
.user-create-form.without-role {
grid-template-columns: 1fr;
}
.topbar {

View File

@@ -30,6 +30,7 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
const [role, setRole] = useState<"admin" | "user">("user");
const [resetTarget, setResetTarget] = useState<ManagedUser | null>(null);
const [resetPassword, setResetPassword] = useState("");
const [deleteTarget, setDeleteTarget] = useState<ManagedUser | null>(null);
const [message, setMessage] = useState("");
const [working, setWorking] = useState(false);
@@ -100,6 +101,31 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
}
};
const deleteUser = async () => {
if (!deleteTarget) return;
setWorking(true);
setMessage("");
try {
const next = await userApi(
await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "delete",
userId: deleteTarget.id,
}),
}),
);
setUsers(next);
setDeleteTarget(null);
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;
@@ -107,6 +133,11 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
return item.role === "user";
};
const canDelete = (item: ManagedUser) => {
if (item.id === currentUser.id || item.role === "super_admin") return false;
return currentUser.role === "super_admin" || item.role === "user";
};
return (
<div className="user-management-layout">
<section className="panel user-create-panel">
@@ -116,7 +147,10 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
<p></p>
</div>
</div>
<form className="user-create-form" onSubmit={createUser}>
<form
className={`user-create-form ${currentUser.role === "super_admin" ? "with-role" : "without-role"}`}
onSubmit={createUser}
>
<label>
<span></span>
<input
@@ -140,7 +174,7 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
/>
</label>
{currentUser.role === "super_admin" && (
<label>
<label className="user-role-field">
<span></span>
<select value={role} onChange={(event) => setRole(event.target.value as "admin" | "user")}>
<option value="user"></option>
@@ -171,11 +205,25 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
<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 className="user-row-actions">
{canReset(item) && (
<button
className="user-action-button"
onClick={() => { setResetTarget(item); setResetPassword(""); }}
>
</button>
)}
{canDelete(item) && (
<button
className="user-action-button danger"
onClick={() => setDeleteTarget(item)}
>
</button>
)}
{!canReset(item) && !canDelete(item) && <span></span>}
</div>
</div>
))}
</div>
@@ -207,6 +255,58 @@ export default function UsersPage({ currentUser }: { currentUser: AuthUser }) {
</form>
</div>
)}
{deleteTarget && (
<div
className="modal-backdrop"
role="presentation"
onMouseDown={() => { if (!working) setDeleteTarget(null); }}
>
<div
className="modal-card compact user-delete-modal"
role="alertdialog"
aria-modal="true"
aria-labelledby="delete-user-title"
onMouseDown={(event) => event.stopPropagation()}
>
<div className="modal-heading">
<div>
<p className="eyebrow danger"></p>
<h2 id="delete-user-title"> {deleteTarget.username}</h2>
</div>
<button
type="button"
onClick={() => setDeleteTarget(null)}
aria-label="关闭"
disabled={working}
>
×
</button>
</div>
<p className="user-delete-warning">
退
</p>
<div className="modal-actions">
<button
type="button"
className="ghost-button"
onClick={() => setDeleteTarget(null)}
disabled={working}
>
</button>
<button
type="button"
className="danger-button"
onClick={() => void deleteUser()}
disabled={working}
>
{working ? "删除中…" : "确认删除"}
</button>
</div>
</div>
</div>
)}
</div>
);
}