2026-08-07 11:06:20 +08:00
|
|
|
import {
|
|
|
|
|
createPasswordRecord,
|
|
|
|
|
getRequestPrincipal,
|
|
|
|
|
managerForbidden,
|
|
|
|
|
normalizeUsername,
|
|
|
|
|
type UserRole,
|
|
|
|
|
validatePassword,
|
|
|
|
|
validateUsername,
|
|
|
|
|
} from "../../../lib/user-auth";
|
|
|
|
|
import { ensureSchema, getRawDb } from "../../../lib/mvp-db";
|
|
|
|
|
|
|
|
|
|
type UserRow = {
|
|
|
|
|
id: string;
|
|
|
|
|
username: string;
|
|
|
|
|
role: UserRole;
|
|
|
|
|
created_at: string;
|
|
|
|
|
updated_at: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function manager(request: Request) {
|
|
|
|
|
const principal = await getRequestPrincipal(request);
|
|
|
|
|
if (!principal || principal.kind !== "user") return null;
|
|
|
|
|
return ["super_admin", "admin"].includes(principal.user.role)
|
|
|
|
|
? principal.user
|
|
|
|
|
: null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function listUsers() {
|
|
|
|
|
const result = await getRawDb()
|
|
|
|
|
.prepare(
|
|
|
|
|
`SELECT id, username, role, created_at, updated_at
|
|
|
|
|
FROM users
|
|
|
|
|
ORDER BY CASE role WHEN 'super_admin' THEN 1 WHEN 'admin' THEN 2 ELSE 3 END,
|
|
|
|
|
created_at ASC`,
|
|
|
|
|
)
|
|
|
|
|
.all<UserRow>();
|
|
|
|
|
return result.results;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function GET(request: Request) {
|
|
|
|
|
await ensureSchema();
|
|
|
|
|
if (!(await manager(request))) return managerForbidden();
|
|
|
|
|
return Response.json({ users: await listUsers() });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function POST(request: Request) {
|
|
|
|
|
await ensureSchema();
|
|
|
|
|
const currentUser = await manager(request);
|
|
|
|
|
if (!currentUser) return managerForbidden();
|
|
|
|
|
try {
|
|
|
|
|
const body = (await request.json()) as {
|
|
|
|
|
action?: string;
|
|
|
|
|
username?: string;
|
|
|
|
|
password?: string;
|
|
|
|
|
role?: UserRole;
|
|
|
|
|
userId?: string;
|
|
|
|
|
};
|
|
|
|
|
const db = getRawDb();
|
|
|
|
|
|
|
|
|
|
if (body.action === "create") {
|
2026-08-07 17:08:56 +08:00
|
|
|
const password = String(body.password ?? "");
|
|
|
|
|
if (!validatePassword(password)) {
|
|
|
|
|
return Response.json({ error: "密码需为8—72位" }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
const passwordRecord = await createPasswordRecord(password);
|
2026-08-07 11:06:20 +08:00
|
|
|
const username = normalizeUsername(body.username);
|
|
|
|
|
const role = body.role === "admin" ? "admin" : "user";
|
|
|
|
|
if (!validateUsername(username)) {
|
|
|
|
|
return Response.json(
|
|
|
|
|
{ error: "账号需为2—32位中文、字母、数字或 _ . @ + -" },
|
|
|
|
|
{ status: 400 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (currentUser.role !== "super_admin" && role !== "user") {
|
|
|
|
|
return managerForbidden();
|
|
|
|
|
}
|
|
|
|
|
await db
|
|
|
|
|
.prepare(
|
|
|
|
|
`INSERT INTO users
|
|
|
|
|
(id, username, password_hash, password_salt, password_iterations, role)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
|
|
|
)
|
|
|
|
|
.bind(
|
|
|
|
|
crypto.randomUUID(),
|
|
|
|
|
username,
|
|
|
|
|
passwordRecord.passwordHash,
|
|
|
|
|
passwordRecord.passwordSalt,
|
|
|
|
|
passwordRecord.passwordIterations,
|
|
|
|
|
role,
|
|
|
|
|
)
|
|
|
|
|
.run();
|
|
|
|
|
} else if (body.action === "reset_password") {
|
2026-08-07 17:08:56 +08:00
|
|
|
const password = String(body.password ?? "");
|
|
|
|
|
if (!validatePassword(password)) {
|
|
|
|
|
return Response.json({ error: "密码需为8—72位" }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
const passwordRecord = await createPasswordRecord(password);
|
2026-08-07 11:06:20 +08:00
|
|
|
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.role === "super_admin" &&
|
|
|
|
|
!(currentUser.role === "super_admin" && target.id === currentUser.id)
|
|
|
|
|
) {
|
|
|
|
|
return Response.json({ error: "不能修改其他超级管理员账号" }, { status: 403 });
|
|
|
|
|
}
|
|
|
|
|
if (currentUser.role !== "super_admin" && target.role !== "user") {
|
|
|
|
|
return managerForbidden();
|
|
|
|
|
}
|
|
|
|
|
await db.batch([
|
|
|
|
|
db
|
|
|
|
|
.prepare(
|
|
|
|
|
`UPDATE users SET
|
|
|
|
|
password_hash = ?, password_salt = ?, password_iterations = ?,
|
|
|
|
|
updated_at = CURRENT_TIMESTAMP
|
|
|
|
|
WHERE id = ?`,
|
|
|
|
|
)
|
|
|
|
|
.bind(
|
|
|
|
|
passwordRecord.passwordHash,
|
|
|
|
|
passwordRecord.passwordSalt,
|
|
|
|
|
passwordRecord.passwordIterations,
|
|
|
|
|
target.id,
|
|
|
|
|
),
|
|
|
|
|
db.prepare("DELETE FROM auth_sessions WHERE user_id = ?").bind(target.id),
|
|
|
|
|
]);
|
2026-08-07 17:08:56 +08:00
|
|
|
} 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),
|
|
|
|
|
]);
|
2026-08-07 11:06:20 +08:00
|
|
|
} else {
|
|
|
|
|
return Response.json({ error: "不支持的操作" }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
return Response.json({ users: await listUsers() });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message = error instanceof Error ? error.message : "操作失败";
|
|
|
|
|
return Response.json(
|
|
|
|
|
{ error: message.includes("UNIQUE") ? "这个登录账号已存在" : message },
|
|
|
|
|
{ status: message.includes("UNIQUE") ? 409 : 500 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|