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

201 lines
6.0 KiB
TypeScript

import db from '../../db'
import bcrypt from 'bcryptjs'
import type { User, Role } from './types'
export const SALT_ROUNDS = 10
export function getRoles(): Role[] {
const stmt = db.prepare('SELECT * FROM roles ORDER BY is_system DESC, id ASC')
const rows = stmt.all() as any[]
return rows.map(row => ({
...row,
permissions: JSON.parse(row.permissions || '[]')
}))
}
export function getRoleByName(name: string): Role | undefined {
const stmt = db.prepare('SELECT * FROM roles WHERE name = ?')
const row = stmt.get(name) as any
if (!row) return undefined
return {
...row,
permissions: JSON.parse(row.permissions || '[]')
}
}
export function createRole(name: string, description: string, permissions: string[], isSystem = false): Role {
const stmt = db.prepare(
'INSERT INTO roles (name, description, permissions, is_system) VALUES (?, ?, ?, ?)'
)
const result = stmt.run(name, description, JSON.stringify(permissions), isSystem ? 1 : 0)
return {
id: Number(result.lastInsertRowid),
name,
description,
permissions,
is_system: isSystem,
created_at: new Date().toISOString()
}
}
export function getUsers(options?: { roleId?: number; status?: string }): User[] {
let sql = 'SELECT u.*, r.name as role_name, r.permissions as role_permissions FROM users u LEFT JOIN roles r ON u.role_id = r.id WHERE 1=1'
const params: any[] = []
if (options?.roleId) {
sql += ' AND u.role_id = ?'
params.push(options.roleId)
}
if (options?.status) {
sql += ' AND u.status = ?'
params.push(options.status)
}
sql += ' ORDER BY u.created_at DESC'
const stmt = db.prepare(sql)
const rows = stmt.all(...params) as any[]
return rows.map(row => ({
...row,
permissions: JSON.parse(row.role_permissions || '[]')
}))
}
export function getUserById(id: number): User | undefined {
const stmt = db.prepare('SELECT u.*, r.name as role_name, r.permissions as role_permissions FROM users u LEFT JOIN roles r ON u.role_id = r.id WHERE u.id = ?')
const row = stmt.get(id) as any
if (!row) return undefined
return {
...row,
permissions: JSON.parse(row.role_permissions || '[]')
}
}
export function getUserByUsername(username: string): User | undefined {
const stmt = db.prepare('SELECT u.*, r.name as role_name, r.permissions as role_permissions FROM users u LEFT JOIN roles r ON u.role_id = r.id WHERE u.username = ?')
const row = stmt.get(username) as any
if (!row) return undefined
return {
...row,
permissions: JSON.parse(row.role_permissions || '[]')
}
}
export function getUserByEmail(email: string): User | undefined {
const stmt = db.prepare('SELECT u.*, r.name as role_name, r.permissions as role_permissions FROM users u LEFT JOIN roles r ON u.role_id = r.id WHERE u.email = ?')
const row = stmt.get(email) as any
if (!row) return undefined
return {
...row,
permissions: JSON.parse(row.role_permissions || '[]')
}
}
export async function createUser(
username: string,
passwordHash: string,
email?: string,
realName?: string,
roleId = 2
): Promise<User> {
const stmt = db.prepare(
'INSERT INTO users (username, password_hash, email, real_name, role_id) VALUES (?, ?, ?, ?, ?)'
)
const result = stmt.run(username, passwordHash, email || null, realName || null, roleId)
return getUserById(Number(result.lastInsertRowid))!
}
export async function updateUser(
id: number,
data: {
email?: string
realName?: string
roleId?: number
status?: string
avatar?: string
}
): Promise<User | undefined> {
const updates: string[] = []
const params: any[] = []
if (data.email !== undefined) {
updates.push('email = ?')
params.push(data.email)
}
if (data.realName !== undefined) {
updates.push('real_name = ?')
params.push(data.realName)
}
if (data.roleId !== undefined) {
updates.push('role_id = ?')
params.push(data.roleId)
}
if (data.status !== undefined) {
updates.push('status = ?')
params.push(data.status)
}
if (data.avatar !== undefined) {
updates.push('avatar = ?')
params.push(data.avatar)
}
if (updates.length === 0) return getUserById(id)
updates.push('updated_at = CURRENT_TIMESTAMP')
params.push(id)
const stmt = db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`)
stmt.run(...params)
return getUserById(id)
}
export async function updatePassword(id: number, newPasswordHash: string): Promise<void> {
const stmt = db.prepare('UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
stmt.run(newPasswordHash, id)
}
export function deleteUser(id: number): boolean {
const stmt = db.prepare('DELETE FROM users WHERE id = ?')
const result = stmt.run(id)
return result.changes > 0
}
export function updateLastLogin(id: number): void {
const stmt = db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?')
stmt.run(id)
}
export function incrementLoginAttempts(id: number, lockedUntil?: Date): void {
if (lockedUntil) {
const stmt = db.prepare('UPDATE users SET login_attempts = login_attempts + 1, locked_until = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
stmt.run(lockedUntil.toISOString(), id)
} else {
const stmt = db.prepare('UPDATE users SET login_attempts = login_attempts + 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
stmt.run(id)
}
}
export function resetLoginAttempts(id: number): void {
const stmt = db.prepare('UPDATE users SET login_attempts = 0, locked_until = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
stmt.run(id)
}
export function hashPassword(password: string): string {
return bcrypt.hashSync(password, SALT_ROUNDS)
}
export function verifyPassword(password: string, hash: string): boolean {
return bcrypt.compareSync(password, hash)
}
export function validatePassword(password: string): { valid: boolean; message?: string } {
if (password.length < 6) {
return { valid: false, message: '密码长度至少6位' }
}
if (/^\d+$/.test(password)) {
return { valid: false, message: '密码不能为纯数字' }
}
return { valid: true }
}