Administrator 8117958bd6 feat: add user center with RBAC, OAuth2 multi-mode and collapsible sidebar
- Add user management with roles and permissions (RBAC)
- Implement OAuth2 service provider supporting 4 grant types:
  authorization_code, password, client_credentials, refresh_token
- Add JWT authentication with 7-day expiry
- Add admin API for users, roles and OAuth clients management
- Add CLI tool for user management (scripts/user-cli.js)
- Add collapsible sidebar layout with login dialog
- Add user management page and OAuth client management page
- Add server middleware for auth token verification
- Add seed script for initial data (admin/admin123)
2026-03-19 17:19:57 +08:00

389 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
import Database from 'better-sqlite3'
import bcrypt from 'bcryptjs'
import { v4 as uuidv4 } from 'uuid'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import fs from 'fs'
import readline from 'readline'
const __dirname = dirname(fileURLToPath(import.meta.url))
const projectRoot = join(__dirname, '..')
const dataDir = join(projectRoot, 'data')
const dbPath = join(dataDir, 'sports.db')
if (!fs.existsSync(dbPath)) {
console.error('❌ 数据库文件不存在,请先运行项目')
process.exit(1)
}
const db = new Database(dbPath)
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
function prompt(question) {
return new Promise((resolve) => rl.question(question, resolve))
}
function validatePassword(password) {
if (password.length < 6) {
return { valid: false, message: '密码长度至少6位' }
}
if (/^\d+$/.test(password)) {
return { valid: false, message: '密码不能为纯数字' }
}
return { valid: true }
}
function initRoles() {
const existingRoles = db.prepare('SELECT COUNT(*) as count FROM roles').get()
if (existingRoles.count === 0) {
console.log('📝 初始化默认角色...')
const defaultRoles = [
{
name: 'admin',
description: '系统管理员',
permissions: JSON.stringify([
'user:create', 'user:read', 'user:update', 'user:delete',
'event:create', 'event:read', 'event:update', 'event:delete',
'result:create', 'result:read', 'result:update', 'result:delete',
'team:create', 'team:read', 'team:update', 'team:delete',
'admin:access', 'role:manage',
'oauth:client:create', 'oauth:client:read', 'oauth:client:update', 'oauth:client:delete'
]),
is_system: 1
},
{
name: 'user',
description: '普通用户',
permissions: JSON.stringify([
'event:read', 'result:read', 'team:read'
]),
is_system: 1
},
{
name: 'guest',
description: '访客',
permissions: JSON.stringify(['event:read']),
is_system: 1
}
]
const stmt = db.prepare('INSERT INTO roles (name, description, permissions, is_system) VALUES (?, ?, ?, ?)')
for (const role of defaultRoles) {
stmt.run(role.name, role.description, role.permissions, role.is_system)
}
console.log('✅ 默认角色初始化完成')
}
}
async function listUsers() {
console.log('\n📋 用户列表:\n')
console.log('ID | 用户名 | 邮箱 | 角色 | 状态 | 创建时间')
console.log('-'.repeat(70))
const users = db.prepare(`
SELECT u.*, r.name as role_name
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
ORDER BY u.created_at DESC
`).all()
if (users.length === 0) {
console.log('暂无用户')
return
}
for (const user of users) {
console.log(
`${user.id} | ${user.username} | ${user.email || '-'} | ${user.role_name} | ${user.status} | ${user.created_at}`
)
}
}
async function createUser() {
const username = await prompt('用户名: ')
if (!username || username.trim().length < 2) {
console.log('❌ 用户名至少2个字符')
return
}
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username)
if (existing) {
console.log('❌ 用户名已存在')
return
}
let password
while (true) {
password = await prompt('密码: ')
const validation = validatePassword(password)
if (validation.valid) break
console.log(`${validation.message}`)
}
const email = await prompt('邮箱 (可选): ')
let roleId = 2
const roles = db.prepare('SELECT id, name FROM roles').all()
if (roles.length > 0) {
console.log('\n可选角色:')
for (const role of roles) {
console.log(` ${role.id}. ${role.name}`)
}
const roleInput = await prompt('角色 ID (默认: 2): ')
if (roleInput && roles.find(r => r.id === parseInt(roleInput))) {
roleId = parseInt(roleInput)
}
}
const passwordHash = bcrypt.hashSync(password, 10)
const stmt = db.prepare(
'INSERT INTO users (username, password_hash, email, role_id) VALUES (?, ?, ?, ?)'
)
const result = stmt.run(username, passwordHash, email || null, roleId)
console.log(`\n✅ 用户创建成功 (ID: ${result.lastInsertRowid})`)
}
async function deleteUser() {
const username = await prompt('要删除的用户名: ')
const user = db.prepare('SELECT id, username, role_id FROM users WHERE username = ?').get(username)
if (!user) {
console.log('❌ 用户不存在')
return
}
const role = db.prepare('SELECT is_system FROM roles WHERE id = ?').get(user.role_id)
if (role?.is_system) {
console.log('❌ 不能删除系统内置用户')
return
}
const confirm = await prompt(`确认删除用户 "${username}" (y/N): `)
if (confirm.toLowerCase() !== 'y') {
console.log('已取消')
return
}
db.prepare('DELETE FROM users WHERE id = ?').run(user.id)
console.log('✅ 用户已删除')
}
async function resetPassword() {
const username = await prompt('用户名: ')
const user = db.prepare('SELECT id FROM users WHERE username = ?').get(username)
if (!user) {
console.log('❌ 用户不存在')
return
}
let password
while (true) {
password = await prompt('新密码: ')
const validation = validatePassword(password)
if (validation.valid) break
console.log(`${validation.message}`)
}
const passwordHash = bcrypt.hashSync(password, 10)
db.prepare('UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(passwordHash, user.id)
console.log('✅ 密码重置成功')
}
async function setRole() {
const username = await prompt('用户名: ')
const user = db.prepare('SELECT id, role_id FROM users WHERE username = ?').get(username)
if (!user) {
console.log('❌ 用户不存在')
return
}
const roles = db.prepare('SELECT id, name FROM roles').all()
console.log('\n可选角色:')
for (const role of roles) {
console.log(` ${role.id}. ${role.name}`)
}
const roleInput = await prompt('新角色 ID: ')
const newRole = roles.find(r => r.id === parseInt(roleInput))
if (!newRole) {
console.log('❌ 无效的角色 ID')
return
}
db.prepare('UPDATE users SET role_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(newRole.id, user.id)
console.log(`✅ 已将 ${username} 的角色设置为 ${newRole.name}`)
}
async function setStatus() {
const username = await prompt('用户名: ')
const user = db.prepare('SELECT id, status FROM users WHERE username = ?').get(username)
if (!user) {
console.log('❌ 用户不存在')
return
}
console.log('\n可选状态:')
console.log(' 1. active (正常)')
console.log(' 2. inactive (停用)')
console.log(' 3. banned (封禁)')
const statusMap = { '1': 'active', '2': 'inactive', '3': 'banned' }
const input = await prompt('新状态: ')
const newStatus = statusMap[input]
if (!newStatus) {
console.log('❌ 无效的状态')
return
}
db.prepare('UPDATE users SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(newStatus, user.id)
console.log(`✅ 用户状态已更新为 ${newStatus}`)
}
async function createOAuthClient() {
const clientName = await prompt('应用名称: ')
if (!clientName) {
console.log('❌ 应用名称不能为空')
return
}
const redirectUrisInput = await prompt('允许的重定向 URI (多个用逗号分隔): ')
const redirectUris = redirectUrisInput.split(',').map(uri => uri.trim()).filter(uri => uri)
console.log('\n支持的授权模式:')
console.log(' 1. authorization_code (授权码模式)')
console.log(' 2. password (密码模式)')
console.log(' 3. client_credentials (客户端凭据模式)')
console.log(' 4. refresh_token (刷新令牌)')
const grantTypeMap = {
'1': 'authorization_code',
'2': 'password',
'3': 'client_credentials',
'4': 'refresh_token'
}
const grantInput = await prompt('授权模式 (多个用逗号分隔默认1): ')
const selectedGrants = grantInput
? grantInput.split(',').map(g => grantTypeMap[g.trim()]).filter(g => g)
: ['authorization_code']
console.log('\n支持的平台:')
console.log(' 1. web (网页应用)')
console.log(' 2. mobile (移动应用)')
console.log(' 3. desktop (桌面应用)')
console.log(' 4. other (其他)')
const platformMap = { '1': 'web', '2': 'mobile', '3': 'desktop', '4': 'other' }
const platformInput = await prompt('平台 (默认1): ')
const platform = platformMap[platformInput] || 'web'
const clientId = uuidv4().replace(/-/g, '')
const clientSecret = uuidv4()
const clientSecretHash = bcrypt.hashSync(clientSecret, 10)
const stmt = db.prepare(`
INSERT INTO oauth_clients
(client_id, client_secret_hash, client_name, redirect_uris, allowed_scopes, grant_types, platform)
VALUES (?, ?, ?, ?, ?, ?, ?)
`)
stmt.run(
clientId,
clientSecretHash,
clientName,
JSON.stringify(redirectUris),
JSON.stringify(['read', 'write']),
JSON.stringify(selectedGrants),
platform
)
console.log('\n✅ OAuth 客户端创建成功!\n')
console.log('='.repeat(50))
console.log('Client ID:')
console.log(clientId)
console.log('\nClient Secret:')
console.log(clientSecret)
console.log('='.repeat(50))
console.log('\n⚠ 请妥善保存 Client Secret它不会再次显示!\n')
}
async function showHelp() {
console.log(`
🔧 运动会管理系统 - 用户管理工具
用法: node scripts/user-cli.js <command>
命令:
list 列出所有用户
create 创建新用户
delete 删除用户
reset-password 重置用户密码
set-role 设置用户角色
set-status 设置用户状态
create-client 创建 OAuth 客户端
init 初始化默认角色
help 显示帮助信息
示例:
node scripts/user-cli.js list
node scripts/user-cli.js create
node scripts/user-cli.js reset-password
`)
}
async function main() {
const args = process.argv.slice(2)
const command = args[0] || 'help'
console.log('🏃 运动会管理系统 - 用户管理 CLI\n')
switch (command) {
case 'list':
await listUsers()
break
case 'create':
await createUser()
break
case 'delete':
await deleteUser()
break
case 'reset-password':
await resetPassword()
break
case 'set-role':
await setRole()
break
case 'set-status':
await setStatus()
break
case 'create-client':
await createOAuthClient()
break
case 'init':
initRoles()
break
case 'help':
default:
await showHelp()
}
rl.close()
}
main().catch(console.error)