feat: 用户管理(xionglin 为管理员)——服务端角色字段与 /api/admin/users 增删改查、客户端管理员专属入口

This commit is contained in:
root
2026-10-16 18:24:56 +08:00
parent cfc8e3c734
commit 3505e10961
10 changed files with 451 additions and 40 deletions
+73 -1
View File
@@ -46,6 +46,20 @@ const SECRET = readJson('secret.key', { secret: 'fallback-secret' }).secret
const registryFile = 'registry.json'
let registry = readJson(registryFile, { users: [] })
// 管理员:xionglin 固定为管理员(存量注册表自动补齐 role 字段)
let adminDirty = false
for (const u of registry.users) {
if (u.username === 'xionglin' && u.role !== 'admin') { u.role = 'admin'; adminDirty = true }
else if (u.username !== 'xionglin' && !u.role) { u.role = 'user'; adminDirty = true }
}
if (adminDirty) saveRegistry()
function isAdminToken(token) {
const auth = verifyToken(token)
if (!auth) return null
const rec = registry.users.find(u => u.id === auth.userId)
if (!rec || rec.role !== 'admin') return null
return rec
}
function saveRegistry() { writeJson(registryFile, registry); try { doBackup() } catch { /* 忽略 */ } }
function userFile(userId) { return path.join(DATA_DIR, 'users', userId + '.json') }
@@ -235,7 +249,7 @@ async function handle(req, res) {
if (registry.users.find(u => u.username === username)) { json(res, 409, { error: '用户名已存在' }); return }
const salt = crypto.randomBytes(16).toString('hex')
const userId = 'cu_' + crypto.randomBytes(6).toString('hex')
registry.users.push({ id: userId, username, pwd_hash: pbkdf2Hash(password, salt), pwd_salt: salt, created_at: new Date().toISOString() })
registry.users.push({ id: userId, username, role: username === 'xionglin' ? 'admin' : 'user', pwd_hash: pbkdf2Hash(password, salt), pwd_salt: salt, created_at: new Date().toISOString() })
saveRegistry()
json(res, 200, { userId, username, token: makeToken(userId) })
return
@@ -346,6 +360,64 @@ async function handle(req, res) {
return
}
// ---------- 用户管理(仅管理员 xionglin) ----------
if (route.startsWith('/api/admin/users')) {
const admin = isAdminToken(String(req.headers['authorization'] || '').replace(/^Bearer\s+/i, '').trim())
if (!admin) { json(res, 403, { error: '需要管理员权限' }); return }
if (req.method === 'GET' && route === '/api/admin/users') {
const list = registry.users.map(u => {
let size = 0
try { size = fs.statSync(userFile(u.id)).size } catch { /* 无数据文件 */ }
return { id: u.id, username: u.username, role: u.role || 'user', created_at: u.created_at, data_bytes: size }
}).sort((x, y) => (x.role === 'admin' ? -1 : 1) - (y.role === 'admin' ? -1 : 1))
json(res, 200, { users: list, me: admin.id })
return
}
if (req.method === 'POST' && route === '/api/admin/users/create') {
const body = await readBody(req)
const username = String(body.username || '').trim()
const password = String(body.password || '')
if (username.length < 2 || username.length > 20) { json(res, 400, { error: '用户名需 2 到 20 个字符' }); return }
if (password.length < 6) { json(res, 400, { error: '密码至少 6 位' }); return }
if (registry.users.find(u => u.username === username)) { json(res, 409, { error: '用户名已存在' }); return }
const salt = crypto.randomBytes(16).toString('hex')
const userId = 'cu_' + crypto.randomBytes(6).toString('hex')
registry.users.push({ id: userId, username, role: username === 'xionglin' ? 'admin' : 'user', pwd_hash: pbkdf2Hash(password, salt), pwd_salt: salt, created_at: new Date().toISOString() })
saveRegistry()
json(res, 200, { ok: true, userId, username })
return
}
if (req.method === 'POST' && route === '/api/admin/users/password') {
const body = await readBody(req)
const rec = registry.users.find(u => u.id === body.userId)
if (!rec) { json(res, 404, { error: '用户不存在' }); return }
if (String(body.newPassword || '').length < 6) { json(res, 400, { error: '密码至少 6 位' }); return }
rec.pwd_salt = crypto.randomBytes(16).toString('hex')
rec.pwd_hash = pbkdf2Hash(String(body.newPassword), rec.pwd_salt)
rec.pwd_changed_at = new Date().toISOString()
saveRegistry()
json(res, 200, { ok: true })
return
}
if (req.method === 'POST' && route === '/api/admin/users/delete') {
const body = await readBody(req)
if (body.userId === admin.id) { json(res, 400, { error: '不能删除自己的账号' }); return }
const idx = registry.users.findIndex(u => u.id === body.userId)
if (idx < 0) { json(res, 404, { error: '用户不存在' }); return }
registry.users.splice(idx, 1)
saveRegistry()
try { fs.rmSync(userFile(body.userId), { force: true }) } catch { /* 忽略 */ }
for (const sseRes of (sseClients.get(body.userId) || [])) { try { sseRes.end() } catch { /* 忽略 */ } }
sseClients.delete(body.userId)
json(res, 200, { ok: true })
return
}
}
json(res, 404, { error: 'not found' })
}