feat: 用户管理(xionglin 为管理员)——服务端角色字段与 /api/admin/users 增删改查、客户端管理员专属入口
This commit is contained in:
+73
-1
@@ -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' })
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user