- 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)
389 lines
11 KiB
JavaScript
389 lines
11 KiB
JavaScript
#!/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)
|