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

127 lines
3.4 KiB
TypeScript

import { z } from 'zod'
import { getClientById, validateRedirectUri, validateScope } from '../../modules/oauth'
import { createAuthorizationCode } from '../../modules/oauth'
const authorizeSchema = z.object({
response_type: z.enum(['code', 'token']),
client_id: z.string().min(1),
redirect_uri: z.string().min(1),
scope: z.string().optional(),
state: z.string().optional(),
code_challenge: z.string().optional(),
code_challenge_method: z.enum(['S256', 'plain']).optional()
})
export default defineEventHandler(async (event) => {
const query = getQuery(event) as Record<string, string>
const result = authorizeSchema.safeParse({
response_type: query.response_type,
client_id: query.client_id,
redirect_uri: query.redirect_uri,
scope: query.scope,
state: query.state,
code_challenge: query.code_challenge,
code_challenge_method: query.code_challenge_method
})
if (!result.success) {
throw createError({
statusCode: 400,
message: result.error.errors[0].message
})
}
const { response_type, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method } = result.data
const client = getClientById(client_id)
if (!client) {
throw createError({
statusCode: 400,
message: '无效的客户端'
})
}
if (!client.is_active) {
throw createError({
statusCode: 400,
message: '客户端已被禁用'
})
}
if (!validateRedirectUri(client, redirect_uri)) {
throw createError({
statusCode: 400,
message: '无效的重定向地址'
})
}
if (scope && !validateScope(client, scope)) {
throw createError({
statusCode: 400,
message: '无效的授权范围'
})
}
if (!client.grant_types.includes('authorization_code') && response_type === 'code') {
throw createError({
statusCode: 400,
message: '客户端不支持授权码模式'
})
}
if (!client.grant_types.includes('implicit') && response_type === 'token') {
throw createError({
statusCode: 400,
message: '客户端不支持隐式授权模式'
})
}
const authHeader = getHeader(event, 'authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw createError({
statusCode: 401,
message: '请先登录'
})
}
const { verifyToken } = await import('../../utils/jwt')
const token = authHeader.substring(7)
const payload = verifyToken(token)
if (!payload || payload.type !== 'access') {
throw createError({
statusCode: 401,
message: '无效的访问令牌'
})
}
if (response_type === 'code') {
const code = createAuthorizationCode(
payload.userId,
client_id,
redirect_uri,
scope,
code_challenge,
code_challenge_method
)
const params = new URLSearchParams({ code })
if (state) params.append('state', state)
return sendRedirect(event, `${redirect_uri}?${params.toString()}`)
} else {
const { createTokenWithoutRefresh } = await import('../../modules/oauth')
const { accessToken, expiresAt } = createTokenWithoutRefresh(payload.userId, client_id, scope)
const fragmentParams = new URLSearchParams({
access_token: accessToken,
token_type: 'Bearer',
expires_in: String(Math.floor((expiresAt.getTime() - Date.now()) / 1000))
})
if (state) fragmentParams.append('state', state)
return sendRedirect(event, `${redirect_uri}#${fragmentParams.toString()}`)
}
})