v3.5.1 版本更新
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-08-19 15:54:51 +08:00
parent 7dd5fdfb12
commit 233ed0af3b
103 changed files with 5764 additions and 185 deletions
@@ -9,8 +9,8 @@ import { JwtAuthGuard } from './jwt-auth.guard';
@Injectable()
export class HqAuthGuard extends JwtAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const ok = super.canActivate(context);
async canActivate(context: ExecutionContext): Promise<boolean> {
const ok = await super.canActivate(context);
if (!ok) return false;
const req = context.switchToHttp().getRequest();
@@ -1,11 +1,13 @@
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { CLIENT_APP_ACTOR_MAP, ClientApp } from '@dukang/shared-types';
import { PrismaService } from '../prisma/prisma.module';
export interface AuthUser {
actorType: string;
@@ -17,11 +19,20 @@ export interface AuthUser {
storeId?: bigint;
}
/**
* v3.5.1 #9:门店/合伙人账号停用或解绑后,运行时接口强制拦截。
* 仅对 STORE / PARTNER actor 做 DB 状态校验;USER / HQ 跳过(不影响 C 端/小程序/总部)。
* 不通过时抛 ForbiddenException({ reason: 'ACCOUNT_DISABLED' }),由 HttpExceptionFilter 透传 reason
* 前端据此 clearAuth() 并跳登录页。
*/
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(protected readonly jwtService: JwtService) {}
constructor(
protected readonly jwtService: JwtService,
protected readonly prisma: PrismaService,
) {}
canActivate(context: ExecutionContext): boolean {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const auth = req.headers.authorization as string | undefined;
if (!auth?.startsWith('Bearer ')) {
@@ -47,10 +58,81 @@ export class JwtAuthGuard implements CanActivate {
? { storeId: BigInt(payload.storeId) }
: {}),
} satisfies AuthUser;
return true;
} catch (err) {
if (err instanceof UnauthorizedException) throw err;
throw new UnauthorizedException('Invalid token');
}
// 账号启停 / 绑定态校验(仅门店与合伙人)
await this.assertAccountActive(req.user);
return true;
}
private async assertAccountActive(user: AuthUser): Promise<void> {
if (user.actorType === 'STORE') {
const acc = await this.prisma.storeAccount.findUnique({
where: { id: user.actorId },
select: { status: true, isTest: true },
});
if (!acc) {
throw new ForbiddenException({
reason: 'ACCOUNT_DISABLED',
message: '门店账号不存在或已停用,请重新登录',
});
}
// 测试门店账号跳过强校验,避免测试环境自锁
if (acc.isTest) return;
if (acc.status !== 'ACTIVE') {
throw new ForbiddenException({
reason: 'ACCOUNT_DISABLED',
message: '门店账号已停用,请重新登录',
});
}
if (user.storeId != null) {
const store = await this.prisma.store.findUnique({
where: { id: user.storeId },
select: { status: true, isTest: true },
});
if (!store) {
throw new ForbiddenException({
reason: 'ACCOUNT_DISABLED',
message: '门店不存在或已关闭,请重新登录',
});
}
if (!store.isTest && store.status !== 'OPEN') {
throw new ForbiddenException({
reason: 'ACCOUNT_DISABLED',
message: '门店已停用或关闭,请重新登录',
});
}
}
return;
}
if (user.actorType === 'PARTNER') {
const acc = await this.prisma.partnerAccount.findUnique({
where: { id: user.actorId },
select: { status: true, bindingStatus: true, isTest: true },
});
if (!acc) {
throw new ForbiddenException({
reason: 'ACCOUNT_DISABLED',
message: '合伙人账号不存在或已停用,请重新登录',
});
}
// 测试合伙人账号跳过强校验,避免测试环境自锁
if (acc.isTest) return;
if (acc.status !== 'ACTIVE' || acc.bindingStatus !== 'ACTIVE') {
throw new ForbiddenException({
reason: 'ACCOUNT_DISABLED',
message: '合伙人账号已停用或解绑,请重新登录',
});
}
return;
}
// USER / HQ 不在此校验(按需求仅门店 + 合伙人)
return;
}
}
@@ -15,7 +15,7 @@ export class PartnerPrimaryGuard implements CanActivate {
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
this.jwtAuthGuard.canActivate(context);
await this.jwtAuthGuard.canActivate(context);
const req = context.switchToHttp().getRequest();
const user = req.user as AuthUser;
if (user.actorType !== 'PARTNER') {