163 lines
4.0 KiB
TypeScript
163 lines
4.0 KiB
TypeScript
/**
|
||
* 认证中间件
|
||
* 用于保护需要登录的 API 路由
|
||
*/
|
||
|
||
import { NextRequest, NextResponse } from 'next/server';
|
||
import { extractTokenFromHeader, TokenPayload } from './jwt';
|
||
import { Role } from '@prisma/client';
|
||
import { getUserInfo, UserInfo } from './permissions';
|
||
import { validateToken } from './token-validator';
|
||
import type { AllowedRole } from './role-scope';
|
||
import { hasAllowedRole } from './role-scope';
|
||
|
||
/**
|
||
* 认证结果
|
||
*/
|
||
export interface AuthResult {
|
||
success: boolean;
|
||
payload?: TokenPayload;
|
||
userInfo?: UserInfo;
|
||
error?: string;
|
||
}
|
||
|
||
/**
|
||
* 验证请求的认证信息
|
||
*/
|
||
export async function authenticateRequest(request: NextRequest): Promise<AuthResult> {
|
||
// 优先从 Authorization 头中读取 Bearer Token(适用于前端显式传入)
|
||
const authHeader = request.headers.get('authorization');
|
||
let token = extractTokenFromHeader(authHeader);
|
||
|
||
// 如果请求头中没有 Token,则退回到 Cookie 中的 auth-token(适用于浏览器自动带上的 Cookie)
|
||
if (!token) {
|
||
const cookieToken = request.cookies.get('auth-token')?.value;
|
||
if (cookieToken) {
|
||
token = cookieToken;
|
||
}
|
||
}
|
||
|
||
if (!token) {
|
||
return {
|
||
success: false,
|
||
error: '未提供认证令牌',
|
||
};
|
||
}
|
||
|
||
const payload = await validateToken<TokenPayload>(token, process.env.JWT_SECRET || '');
|
||
|
||
if (!payload) {
|
||
return {
|
||
success: false,
|
||
error: '认证令牌无效或已过期',
|
||
};
|
||
}
|
||
|
||
// 获取用户完整信息(包括管理关系)
|
||
const userInfo = await getUserInfo(payload.userId);
|
||
|
||
if (!userInfo) {
|
||
return {
|
||
success: false,
|
||
error: '用户不存在',
|
||
};
|
||
}
|
||
|
||
// 检查用户状态
|
||
if (userInfo.status === 'PENDING') {
|
||
return {
|
||
success: false,
|
||
error: '账号待管理员激活',
|
||
};
|
||
}
|
||
|
||
if (userInfo.status !== 'ACTIVE') {
|
||
return {
|
||
success: false,
|
||
error: '账号已被停用',
|
||
};
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
payload,
|
||
userInfo,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否有指定角色
|
||
*/
|
||
export function hasRole(userInfo: UserInfo, allowedRoles: readonly AllowedRole[]): boolean {
|
||
return hasAllowedRole(userInfo, allowedRoles);
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否为管理员(超级管理员或管理员)
|
||
*/
|
||
export function isAdmin(payload: TokenPayload): boolean {
|
||
return ['SUPER_ADMIN', 'ADMIN'].includes(payload.role);
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否为部门管理员或更高权限
|
||
*/
|
||
export function isDepartmentManagerOrAbove(payload: TokenPayload): boolean {
|
||
return ['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR'].includes(payload.role);
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否为组长或更高权限
|
||
*/
|
||
export function isTeamLeaderOrAbove(payload: TokenPayload): boolean {
|
||
return ['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR'].includes(payload.role);
|
||
}
|
||
|
||
/**
|
||
* 创建未授权响应
|
||
*/
|
||
export function unauthorizedResponse(message: string = '未授权访问'): NextResponse {
|
||
return NextResponse.json(
|
||
{ error: message },
|
||
{ status: 401 }
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 创建禁止访问响应
|
||
*/
|
||
export function forbiddenResponse(message: string = '权限不足'): NextResponse {
|
||
return NextResponse.json(
|
||
{ error: message },
|
||
{ status: 403 }
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 认证中间件包装器
|
||
* 用于包装需要认证的 API 处理函数
|
||
*/
|
||
export function withAuth(
|
||
handler: (request: NextRequest, payload: TokenPayload, userInfo: UserInfo) => Promise<NextResponse>,
|
||
options?: {
|
||
allowedRoles?: AllowedRole[];
|
||
}
|
||
) {
|
||
return async (request: NextRequest): Promise<NextResponse> => {
|
||
// 验证认证
|
||
const authResult = await authenticateRequest(request);
|
||
|
||
if (!authResult.success || !authResult.payload || !authResult.userInfo) {
|
||
return unauthorizedResponse(authResult.error);
|
||
}
|
||
|
||
// 检查角色权限
|
||
if (options?.allowedRoles && !hasRole(authResult.userInfo, options.allowedRoles)) {
|
||
return forbiddenResponse('您没有权限访问此资源');
|
||
}
|
||
|
||
// 调用实际的处理函数
|
||
return handler(request, authResult.payload, authResult.userInfo);
|
||
};
|
||
}
|