2026-04-17 12:50:06 +08:00

163 lines
4.0 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.

/**
* 认证中间件
* 用于保护需要登录的 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);
};
}