feat(auth): 平台超管租户切换(super 标志 + switch-tenant)
- JWT 加 super 标志,sub 保持超管真实 userId,tenantCode 随切换 - isSuper 解析/checkUserActive/PermissionsGuard 支持跨租户超管 - POST /auth/switch-tenant Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
477f35293e
commit
6618137da5
@ -1,7 +1,7 @@
|
|||||||
import { Body, Controller, Get, Patch, Post, Req, UnauthorizedException } from '@nestjs/common';
|
import { Body, Controller, Get, Patch, Post, Req, UnauthorizedException } from '@nestjs/common';
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
import { AuthService } from './auth.service.js';
|
import { AuthService } from './auth.service.js';
|
||||||
import { ChangePasswordDto, LoginDto, RefreshDto, RegisterDto } from './dto.js';
|
import { ChangePasswordDto, LoginDto, RefreshDto, RegisterDto, SwitchTenantDto } from './dto.js';
|
||||||
import { Public } from './decorators.js';
|
import { Public } from './decorators.js';
|
||||||
import { getRequestContext } from '../common/context/request-context.js';
|
import { getRequestContext } from '../common/context/request-context.js';
|
||||||
|
|
||||||
@ -38,7 +38,7 @@ export class AuthController {
|
|||||||
menus() {
|
menus() {
|
||||||
const ctx = getRequestContext();
|
const ctx = getRequestContext();
|
||||||
if (!ctx) throw new UnauthorizedException('未登录');
|
if (!ctx) throw new UnauthorizedException('未登录');
|
||||||
return this.auth.getMenus(ctx.tenantCode, ctx.userId);
|
return this.auth.getMenus(ctx.tenantCode, ctx.userId, ctx.super);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前用户权限码(含超管标志),供前端按钮/路由级权限控制(仅需登录)。 */
|
/** 当前用户权限码(含超管标志),供前端按钮/路由级权限控制(仅需登录)。 */
|
||||||
@ -46,7 +46,15 @@ export class AuthController {
|
|||||||
permissions() {
|
permissions() {
|
||||||
const ctx = getRequestContext();
|
const ctx = getRequestContext();
|
||||||
if (!ctx) throw new UnauthorizedException('未登录');
|
if (!ctx) throw new UnauthorizedException('未登录');
|
||||||
return this.auth.getPermissionSet(ctx.tenantCode, ctx.userId);
|
return this.auth.getPermissionSet(ctx.tenantCode, ctx.userId, ctx.super);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 平台超管切换租户(仅需登录;内部校验调用者为平台超管)。 */
|
||||||
|
@Post('switch-tenant')
|
||||||
|
switchTenant(@Body() dto: SwitchTenantDto, @Req() req: Request) {
|
||||||
|
const ctx = getRequestContext();
|
||||||
|
if (!ctx) throw new UnauthorizedException('未登录');
|
||||||
|
return this.auth.switchTenant(ctx.userId, dto, req.ip, req.headers['user-agent']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前租户品牌(仅需登录)。 */
|
/** 当前租户品牌(仅需登录)。 */
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { BadRequestException, ConflictException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
|
import { BadRequestException, ConflictException, ForbiddenException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { createHash, randomUUID } from 'node:crypto';
|
import { createHash, randomUUID } from 'node:crypto';
|
||||||
import { PrismaService } from '../prisma/prisma.service.js';
|
import { PrismaService } from '../prisma/prisma.service.js';
|
||||||
import { RedisService } from '../common/redis/redis.service.js';
|
import { RedisService } from '../common/redis/redis.service.js';
|
||||||
import { LoginDto, RefreshDto, RegisterDto, ChangePasswordDto } from './dto.js';
|
import { ChangePasswordDto, LoginDto, RefreshDto, RegisterDto, SwitchTenantDto } from './dto.js';
|
||||||
import { resolveDataScope, resolveVisibleOrgs } from '../common/data-scope.js';
|
import { resolveDataScope, resolveVisibleOrgs } from '../common/data-scope.js';
|
||||||
import type { DataScope } from '../common/enums.js';
|
import type { DataScope } from '../common/enums.js';
|
||||||
import type { RequestContext } from '../common/context/request-context.js';
|
import type { RequestContext } from '../common/context/request-context.js';
|
||||||
@ -303,18 +303,20 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 读取用户权限集;缓存缺失时重新聚合并回填(Redis 重启自愈)。 */
|
/** 读取用户权限集;缓存缺失时重新聚合并回填(Redis 重启自愈)。 */
|
||||||
async getPermissionSet(tenantCode: string, userId: string): Promise<PermissionSet> {
|
async getPermissionSet(tenantCode: string, userId: string, switched = false): Promise<PermissionSet> {
|
||||||
const cached = await this.redis.getJson<PermissionSet>(this.permKey(tenantCode, userId));
|
const cached = await this.redis.getJson<PermissionSet>(this.permKey(tenantCode, userId));
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
return this.writePermissionCache(tenantCode, userId);
|
return this.writePermissionCache(tenantCode, userId, switched);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 当前用户的菜单树(超管返回全部启用菜单,否则按角色可见菜单 + 补祖先)。 */
|
/** 当前用户的菜单树(超管返回全部启用菜单,否则按角色可见菜单 + 补祖先)。 */
|
||||||
async getMenus(tenantCode: string, userId: string): Promise<MenuNode[]> {
|
async getMenus(tenantCode: string, userId: string, switched = false): Promise<MenuNode[]> {
|
||||||
const roleRows = await this.prisma.client.userRole.findMany({
|
const roleRows = switched
|
||||||
where: { tenantCode, userId },
|
? []
|
||||||
select: { roleCode: true },
|
: await this.prisma.client.userRole.findMany({
|
||||||
});
|
where: { tenantCode, userId },
|
||||||
|
select: { roleCode: true },
|
||||||
|
});
|
||||||
const roleCodes = roleRows.map((r) => r.roleCode);
|
const roleCodes = roleRows.map((r) => r.roleCode);
|
||||||
// 过滤已删除角色(与 resolveAuth 保持一致)
|
// 过滤已删除角色(与 resolveAuth 保持一致)
|
||||||
const activeRoles = roleCodes.length
|
const activeRoles = roleCodes.length
|
||||||
@ -324,7 +326,7 @@ export class AuthService {
|
|||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const activeRoleCodes = activeRoles.map((r) => r.code);
|
const activeRoleCodes = activeRoles.map((r) => r.code);
|
||||||
const isSuper = activeRoleCodes.includes(SUPER_ROLE);
|
const isSuper = switched || activeRoleCodes.includes(SUPER_ROLE);
|
||||||
|
|
||||||
const rows = isSuper
|
const rows = isSuper
|
||||||
? await this.prisma.client.resource.findMany({
|
? await this.prisma.client.resource.findMany({
|
||||||
@ -368,19 +370,23 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 由 RequestContextInterceptor 调用,构建当前请求的租户/用户/数据权限上下文。 */
|
/** 由 RequestContextInterceptor 调用,构建当前请求的租户/用户/数据权限上下文。 */
|
||||||
async buildContext(user: { userId: string; tenantCode: string; username: string }): Promise<RequestContext> {
|
async buildContext(user: { userId: string; tenantCode: string; username: string; super?: boolean }): Promise<RequestContext> {
|
||||||
const u = await this.prisma.client.user.findFirst({
|
const switched = user.super ?? false;
|
||||||
where: { tenantCode: user.tenantCode, id: user.userId, deletedAt: null },
|
let orgCode: string | null = null;
|
||||||
select: { orgCode: true },
|
let roleCodes: string[] = [];
|
||||||
});
|
if (!switched) {
|
||||||
const orgCode = u?.orgCode ?? null;
|
const u = await this.prisma.client.user.findFirst({
|
||||||
|
where: { tenantCode: user.tenantCode, id: user.userId, deletedAt: null },
|
||||||
const roleRows = await this.prisma.client.userRole.findMany({
|
select: { orgCode: true },
|
||||||
where: { tenantCode: user.tenantCode, userId: user.userId },
|
});
|
||||||
select: { roleCode: true },
|
orgCode = u?.orgCode ?? null;
|
||||||
});
|
const roleRows = await this.prisma.client.userRole.findMany({
|
||||||
const roleCodes = roleRows.map((r) => r.roleCode);
|
where: { tenantCode: user.tenantCode, userId: user.userId },
|
||||||
const isSuper = roleCodes.includes(SUPER_ROLE);
|
select: { roleCode: true },
|
||||||
|
});
|
||||||
|
roleCodes = roleRows.map((r) => r.roleCode);
|
||||||
|
}
|
||||||
|
const isSuper = switched || roleCodes.includes(SUPER_ROLE);
|
||||||
const roles = roleCodes.length
|
const roles = roleCodes.length
|
||||||
? await this.prisma.client.role.findMany({
|
? await this.prisma.client.role.findMany({
|
||||||
where: { tenantCode: user.tenantCode, code: { in: roleCodes }, deletedAt: null },
|
where: { tenantCode: user.tenantCode, code: { in: roleCodes }, deletedAt: null },
|
||||||
@ -398,7 +404,15 @@ export class AuthService {
|
|||||||
roleCodes,
|
roleCodes,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { tenantCode: user.tenantCode, userId: user.userId, username: user.username, dataScope, orgCode, orgCodes };
|
return {
|
||||||
|
tenantCode: user.tenantCode,
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
super: isSuper,
|
||||||
|
dataScope,
|
||||||
|
orgCode,
|
||||||
|
orgCodes,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private permKey(tenantCode: string, userId: string): string {
|
private permKey(tenantCode: string, userId: string): string {
|
||||||
@ -410,8 +424,10 @@ export class AuthService {
|
|||||||
tenantCode: string,
|
tenantCode: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
tokenVersion: number,
|
tokenVersion: number,
|
||||||
|
switched = false,
|
||||||
): Promise<{ active: boolean; mustChangePassword: boolean }> {
|
): Promise<{ active: boolean; mustChangePassword: boolean }> {
|
||||||
const key = userStateCacheKey(tenantCode, userId);
|
const homeTenant = switched ? PLATFORM_TENANT_CODE : tenantCode;
|
||||||
|
const key = userStateCacheKey(homeTenant, userId);
|
||||||
let state = await this.redis.getJson<{
|
let state = await this.redis.getJson<{
|
||||||
status: string;
|
status: string;
|
||||||
tokenVersion: number;
|
tokenVersion: number;
|
||||||
@ -420,7 +436,7 @@ export class AuthService {
|
|||||||
}>(key);
|
}>(key);
|
||||||
if (!state) {
|
if (!state) {
|
||||||
const user = await this.prisma.client.user.findFirst({
|
const user = await this.prisma.client.user.findFirst({
|
||||||
where: { tenantCode, id: userId, deletedAt: null },
|
where: { tenantCode: homeTenant, id: userId, deletedAt: null },
|
||||||
select: { status: true, tokenVersion: true, lockedAt: true, mustChangePassword: true },
|
select: { status: true, tokenVersion: true, lockedAt: true, mustChangePassword: true },
|
||||||
});
|
});
|
||||||
if (!user) return { active: false, mustChangePassword: false };
|
if (!user) return { active: false, mustChangePassword: false };
|
||||||
@ -460,12 +476,64 @@ export class AuthService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 聚合权限码与超管标志。 */
|
/** 平台超管是否持有 super_admin 角色(查平台租户,不随切换租户变化)。 */
|
||||||
private async resolveAuth(tenantCode: string, userId: string): Promise<PermissionSet> {
|
private async isPlatformSuperAdmin(userId: string): Promise<boolean> {
|
||||||
const roleRows = await this.prisma.client.userRole.findMany({
|
const row = await this.prisma.raw.userRole.findFirst({
|
||||||
where: { tenantCode, userId },
|
where: { tenantCode: PLATFORM_TENANT_CODE, userId, roleCode: SUPER_ROLE },
|
||||||
select: { roleCode: true },
|
select: { userId: true },
|
||||||
});
|
});
|
||||||
|
return row != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 平台超管切入任意租户:签发 super=true 的新 token(sub 保持超管真实 userId)。 */
|
||||||
|
async switchTenant(
|
||||||
|
userId: string,
|
||||||
|
dto: SwitchTenantDto,
|
||||||
|
ip?: string,
|
||||||
|
userAgent?: string,
|
||||||
|
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number; tenantCode: string; tenantName: string }> {
|
||||||
|
if (!(await this.isPlatformSuperAdmin(userId))) {
|
||||||
|
throw new ForbiddenException('仅平台超级管理员可切换租户');
|
||||||
|
}
|
||||||
|
const admin = await this.prisma.raw.user.findFirst({
|
||||||
|
where: { tenantCode: PLATFORM_TENANT_CODE, id: userId, deletedAt: null },
|
||||||
|
});
|
||||||
|
if (!admin || admin.status !== 'enabled' || admin.lockedAt) {
|
||||||
|
throw new BusinessException(ErrorCodes.AUTH_DISABLED, '平台超管账号不可用', HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
const tenant = await this.prisma.raw.tenant.findUnique({ where: { code: dto.tenantCode } });
|
||||||
|
if (!tenant || tenant.status !== 'enabled') {
|
||||||
|
throw new BusinessException(ErrorCodes.TENANT_NOT_FOUND, '目标租户不存在或已停用', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
const tokens = await this.issue(
|
||||||
|
dto.tenantCode,
|
||||||
|
userId,
|
||||||
|
admin.username,
|
||||||
|
admin.tokenVersion,
|
||||||
|
{ sid: randomUUID(), loginAt: new Date(), ip: ip ?? null, userAgent: userAgent ?? null },
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
await this.recordAudit({
|
||||||
|
tenantCode: dto.tenantCode,
|
||||||
|
userId,
|
||||||
|
username: admin.username,
|
||||||
|
module: 'auth',
|
||||||
|
action: 'switch-tenant',
|
||||||
|
status: 'success',
|
||||||
|
ip: ip ?? null,
|
||||||
|
userAgent: userAgent ?? null,
|
||||||
|
});
|
||||||
|
return { ...tokens, tenantCode: dto.tenantCode, tenantName: tenant.name };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 聚合权限码与超管标志。 */
|
||||||
|
private async resolveAuth(tenantCode: string, userId: string, switched = false): Promise<PermissionSet> {
|
||||||
|
const roleRows = switched
|
||||||
|
? []
|
||||||
|
: await this.prisma.client.userRole.findMany({
|
||||||
|
where: { tenantCode, userId },
|
||||||
|
select: { roleCode: true },
|
||||||
|
});
|
||||||
const roleCodes = roleRows.map((r) => r.roleCode);
|
const roleCodes = roleRows.map((r) => r.roleCode);
|
||||||
// 过滤已删除角色(软删角色不参与权限聚合)
|
// 过滤已删除角色(软删角色不参与权限聚合)
|
||||||
const activeRoles = roleCodes.length
|
const activeRoles = roleCodes.length
|
||||||
@ -475,7 +543,7 @@ export class AuthService {
|
|||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const activeRoleCodes = activeRoles.map((r) => r.code);
|
const activeRoleCodes = activeRoles.map((r) => r.code);
|
||||||
const isSuper = activeRoleCodes.includes(SUPER_ROLE);
|
const isSuper = switched || activeRoleCodes.includes(SUPER_ROLE);
|
||||||
const resourceRows = activeRoleCodes.length
|
const resourceRows = activeRoleCodes.length
|
||||||
? await this.prisma.client.roleResource.findMany({
|
? await this.prisma.client.roleResource.findMany({
|
||||||
where: { tenantCode, roleCode: { in: activeRoleCodes } },
|
where: { tenantCode, roleCode: { in: activeRoleCodes } },
|
||||||
@ -495,8 +563,8 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 聚合并写入权限缓存(登录/刷新时调用)。 */
|
/** 聚合并写入权限缓存(登录/刷新时调用)。 */
|
||||||
private async writePermissionCache(tenantCode: string, userId: string): Promise<PermissionSet> {
|
private async writePermissionCache(tenantCode: string, userId: string, switched = false): Promise<PermissionSet> {
|
||||||
const set = await this.resolveAuth(tenantCode, userId);
|
const set = await this.resolveAuth(tenantCode, userId, switched);
|
||||||
await this.redis.setJson(this.permKey(tenantCode, userId), set, REFRESH_TTL);
|
await this.redis.setJson(this.permKey(tenantCode, userId), set, REFRESH_TTL);
|
||||||
return set;
|
return set;
|
||||||
}
|
}
|
||||||
@ -538,8 +606,16 @@ export class AuthService {
|
|||||||
username: string,
|
username: string,
|
||||||
tokenVersion: number,
|
tokenVersion: number,
|
||||||
session: { sid: string; loginAt: Date; ip?: string | null; userAgent?: string | null },
|
session: { sid: string; loginAt: Date; ip?: string | null; userAgent?: string | null },
|
||||||
|
switched = false,
|
||||||
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
|
||||||
const accessToken = this.jwt.sign({ sub: userId, tenantCode, username, ver: tokenVersion, sid: session.sid });
|
const accessToken = this.jwt.sign({
|
||||||
|
sub: userId,
|
||||||
|
tenantCode,
|
||||||
|
username,
|
||||||
|
ver: tokenVersion,
|
||||||
|
sid: session.sid,
|
||||||
|
...(switched ? { super: true } : {}),
|
||||||
|
});
|
||||||
const jti = randomUUID();
|
const jti = randomUUID();
|
||||||
await this.prisma.client.refreshToken.create({
|
await this.prisma.client.refreshToken.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@ -65,3 +65,10 @@ export class RefreshDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
refreshToken!: string;
|
refreshToken!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SwitchTenantDto {
|
||||||
|
@ApiProperty({ description: '目标租户编码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
tenantCode!: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export interface AuthUser {
|
|||||||
userId: string;
|
userId: string;
|
||||||
tenantCode: string;
|
tenantCode: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
super?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -34,8 +35,8 @@ export class JwtAuthGuard implements CanActivate {
|
|||||||
const token = this.extractToken(request);
|
const token = this.extractToken(request);
|
||||||
if (!token) throw new UnauthorizedException('未登录');
|
if (!token) throw new UnauthorizedException('未登录');
|
||||||
try {
|
try {
|
||||||
const payload = await this.jwt.verifyAsync<{ sub: string; tenantCode: string; username: string; ver?: number; sid?: string }>(token);
|
const payload = await this.jwt.verifyAsync<{ sub: string; tenantCode: string; username: string; ver?: number; sid?: string; super?: boolean }>(token);
|
||||||
const { active, mustChangePassword } = await this.auth.checkUserActive(payload.tenantCode, payload.sub, payload.ver ?? 0);
|
const { active, mustChangePassword } = await this.auth.checkUserActive(payload.tenantCode, payload.sub, payload.ver ?? 0, payload.super ?? false);
|
||||||
if (!active) throw new UnauthorizedException('账号已停用或登录已失效');
|
if (!active) throw new UnauthorizedException('账号已停用或登录已失效');
|
||||||
if (payload.sid && (await this.auth.isSessionRevoked(payload.sid))) {
|
if (payload.sid && (await this.auth.isSessionRevoked(payload.sid))) {
|
||||||
throw new BusinessException(ErrorCodes.AUTH_SESSION_REVOKED, '会话已失效,请重新登录', HttpStatus.UNAUTHORIZED);
|
throw new BusinessException(ErrorCodes.AUTH_SESSION_REVOKED, '会话已失效,请重新登录', HttpStatus.UNAUTHORIZED);
|
||||||
@ -43,7 +44,12 @@ export class JwtAuthGuard implements CanActivate {
|
|||||||
if (mustChangePassword && !this.isAllowedWhileMustChange(request)) {
|
if (mustChangePassword && !this.isAllowedWhileMustChange(request)) {
|
||||||
throw new BusinessException(ErrorCodes.AUTH_MUST_CHANGE_PWD, '请先修改初始密码', HttpStatus.FORBIDDEN);
|
throw new BusinessException(ErrorCodes.AUTH_MUST_CHANGE_PWD, '请先修改初始密码', HttpStatus.FORBIDDEN);
|
||||||
}
|
}
|
||||||
request.user = { userId: payload.sub, tenantCode: payload.tenantCode, username: payload.username } satisfies AuthUser;
|
request.user = {
|
||||||
|
userId: payload.sub,
|
||||||
|
tenantCode: payload.tenantCode,
|
||||||
|
username: payload.username,
|
||||||
|
super: payload.super ?? false,
|
||||||
|
} satisfies AuthUser;
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof HttpException) throw e;
|
if (e instanceof HttpException) throw e;
|
||||||
|
|||||||
@ -18,10 +18,10 @@ export class PermissionsGuard implements CanActivate {
|
|||||||
if (!required || required.length === 0) return true;
|
if (!required || required.length === 0) return true;
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const user = request.user as { userId: string; tenantCode: string } | undefined;
|
const user = request.user as { userId: string; tenantCode: string; super?: boolean } | undefined;
|
||||||
if (!user) throw new ForbiddenException('无权限');
|
if (!user) throw new ForbiddenException('无权限');
|
||||||
|
|
||||||
const { super: isSuper, permissions } = await this.auth.getPermissionSet(user.tenantCode, user.userId);
|
const { super: isSuper, permissions } = await this.auth.getPermissionSet(user.tenantCode, user.userId, user.super ?? false);
|
||||||
if (isSuper) return true;
|
if (isSuper) return true;
|
||||||
if (required.every((code) => permissions.includes(code))) return true;
|
if (required.every((code) => permissions.includes(code))) return true;
|
||||||
throw new ForbiddenException('无权限');
|
throw new ForbiddenException('无权限');
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export class RequestContextInterceptor implements NestInterceptor {
|
|||||||
|
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const user = request.user as { userId: string; tenantCode: string; username: string } | undefined;
|
const user = request.user as { userId: string; tenantCode: string; username: string; super?: boolean } | undefined;
|
||||||
if (!user) return next.handle();
|
if (!user) return next.handle();
|
||||||
|
|
||||||
return from(this.auth.buildContext(user)).pipe(
|
return from(this.auth.buildContext(user)).pipe(
|
||||||
|
|||||||
@ -6,6 +6,8 @@ export interface RequestContext {
|
|||||||
tenantCode: string;
|
tenantCode: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
/** 是否平台超管(含跨租户切换后的超管)。 */
|
||||||
|
super: boolean;
|
||||||
dataScope: DataScope;
|
dataScope: DataScope;
|
||||||
orgCode: string | null;
|
orgCode: string | null;
|
||||||
/** 数据权限可见的部门 code 集合;空数组表示「全部可见」 */
|
/** 数据权限可见的部门 code 集合;空数组表示「全部可见」 */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user