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

99 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

import { z } from 'zod'
import { getUserByUsername, verifyPassword, resetLoginAttempts, updateLastLogin, incrementLoginAttempts, validatePassword } from '../../modules/auth/user'
import { createToken } from '../../modules/oauth'
import { generateAccessToken, generateRefreshToken } from '../../utils/jwt'
const loginSchema = z.object({
username: z.string().min(1, '用户名不能为空'),
password: z.string().min(1, '密码不能为空')
})
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const result = loginSchema.safeParse(body)
if (!result.success) {
throw createError({
statusCode: 400,
message: result.error.errors[0].message
})
}
const { username, password } = result.data
const user = getUserByUsername(username)
if (!user) {
throw createError({
statusCode: 401,
message: '用户名或密码错误'
})
}
if (user.status !== 'active') {
throw createError({
statusCode: 403,
message: '账户已被禁用'
})
}
if (user.locked_until && new Date(user.locked_until) > new Date()) {
throw createError({
statusCode: 423,
message: `账户已被锁定,请于 ${new Date(user.locked_until).toLocaleString()} 后重试`
})
}
if (!verifyPassword(password, user.password_hash!)) {
incrementLoginAttempts(user.id)
const attempts = (user.login_attempts || 0) + 1
if (attempts >= 5) {
const lockedUntil = new Date(Date.now() + 30 * 60 * 1000)
incrementLoginAttempts(user.id, lockedUntil)
throw createError({
statusCode: 423,
message: '密码错误次数过多账户已被锁定30分钟'
})
}
throw createError({
statusCode: 401,
message: `用户名或密码错误(剩余${5 - attempts}次)`
})
}
resetLoginAttempts(user.id)
updateLastLogin(user.id)
const payload = {
sub: user.username,
userId: user.id,
username: user.username,
role: user.role_name!,
permissions: user.permissions || []
}
const { accessToken, refreshToken } = createToken(user.id, 'system')
const jwtAccessToken = generateAccessToken(payload)
const jwtRefreshToken = generateRefreshToken(payload)
return {
success: true,
data: {
accessToken: jwtAccessToken,
refreshToken: jwtRefreshToken,
expiresIn: 7 * 24 * 60 * 60,
tokenType: 'Bearer',
user: {
id: user.id,
username: user.username,
email: user.email,
realName: user.real_name,
avatar: user.avatar,
role: user.role_name,
permissions: user.permissions
}
}
}
})