- 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)
127 lines
3.4 KiB
TypeScript
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()}`)
|
|
}
|
|
})
|