- 平台管理员模型:super_admin=平台超管、tenant_super_admin=租户顶层(保留码),tenant:* 仅平台可授予 - 软删改名回收唯一键 + P2002→409 - 删除角色级联清理关联 + 聚合过滤已删/停用角色资源 - tokenVersion 会话吊销(重置密码/停用/删除即时踢下线) - data_scope self 按 userId 过滤 + 详情数据权限校验 - 组织树防环、seed 不重置密码、日志脱敏补 phone/email - JWT 密钥强校验、pageSize 上限、DTO 枚举校验、Prisma 错误映射、授权事务化 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
53 lines
2.0 KiB
TypeScript
53 lines
2.0 KiB
TypeScript
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { IS_PUBLIC_KEY } from './decorators.js';
|
|
import { AuthService } from './auth.service.js';
|
|
|
|
export interface AuthUser {
|
|
userId: string;
|
|
tenantCode: string;
|
|
username: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class JwtAuthGuard implements CanActivate {
|
|
constructor(
|
|
private readonly jwt: JwtService,
|
|
private readonly reflector: Reflector,
|
|
private readonly auth: AuthService,
|
|
) {}
|
|
|
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
context.getHandler(),
|
|
context.getClass(),
|
|
]);
|
|
if (isPublic) return true;
|
|
|
|
const request = context.switchToHttp().getRequest();
|
|
const url: string = request.originalUrl ?? request.url ?? '';
|
|
if (url.startsWith('/docs')) return true;
|
|
|
|
const token = this.extractToken(request);
|
|
if (!token) throw new UnauthorizedException('未登录');
|
|
try {
|
|
const payload = await this.jwt.verifyAsync<{ sub: string; tenantCode: string; username: string; ver?: number }>(token);
|
|
const active = await this.auth.checkUserActive(payload.tenantCode, payload.sub, payload.ver ?? 0);
|
|
if (!active) throw new UnauthorizedException('账号已停用或登录已失效');
|
|
request.user = { userId: payload.sub, tenantCode: payload.tenantCode, username: payload.username } satisfies AuthUser;
|
|
return true;
|
|
} catch (e) {
|
|
if (e instanceof UnauthorizedException) throw e;
|
|
throw new UnauthorizedException('token 无效或已过期');
|
|
}
|
|
}
|
|
|
|
private extractToken(request: { headers?: Record<string, string | undefined> }): string | undefined {
|
|
const auth = request.headers?.authorization;
|
|
if (!auth) return undefined;
|
|
const [type, token] = auth.split(' ');
|
|
return type === 'Bearer' && token ? token : undefined;
|
|
}
|
|
}
|