Files
dukang/server/dukang-api/src/common/guards/hq-permission.guard.ts
T
jacy 5935024ea8
CI / verify (pull_request) Waiting to run
v3.5.8和v3.5.9版本更新
2026-08-25 09:20:32 +08:00

192 lines
5.8 KiB
TypeScript

import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
NotFoundException,
SetMetadata,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import {
HQ_PERMISSION_CATALOG,
HQ_ROLE_DEFAULT_PERMISSIONS,
computeHqEffectivePermissionKeys,
expandHqPermissionKeys,
hasAnySystemSettingsPermission,
type HqAdminRoleValue,
type HqPermissionKey,
} from '@dukang/shared-types';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.module';
import type { AuthUser } from './jwt-auth.guard';
export const HQ_PERMISSIONS_KEY = 'hq:permissions';
export const RequireHqPermissions = (...keys: string[]) =>
SetMetadata(HQ_PERMISSIONS_KEY, keys);
/** 任意一项系统设置分组权限即可访问系统设置接口 */
export const RequireAnySystemSettings = () =>
SetMetadata(HQ_PERMISSIONS_KEY, ['__any_system_settings__']);
/** null = 全国;[] = 无可见城市 */
export type HqCityScope = bigint[] | null;
export function hqStoreCityWhere(scope: HqCityScope): Prisma.StoreWhereInput | undefined {
if (scope === null) return undefined;
if (!scope.length) return { id: { equals: BigInt(0) } };
return { cityId: { in: scope } };
}
export function mergeHqStoreCityWhere(
where: Prisma.StoreWhereInput,
scope: HqCityScope,
requestedCityId?: string,
): Prisma.StoreWhereInput {
if (requestedCityId) {
const cityId = BigInt(requestedCityId);
assertHqCityInScope(scope, cityId);
return { AND: [where, { cityId }] };
}
const scoped = hqStoreCityWhere(scope);
if (!scoped) return where;
return { AND: [where, scoped] };
}
export function assertHqCityInScope(scope: HqCityScope, cityId: bigint) {
if (scope === null) return;
if (!scope.some((id) => id === cityId)) {
throw new ForbiddenException('无权访问该城市的门店');
}
}
@Injectable()
export class HqPermissionsResolver {
constructor(private readonly prisma: PrismaService) {}
private async loadActiveAccount(actorId: bigint) {
const account = await this.prisma.hqAccount.findUnique({
where: { id: actorId },
select: { adminRole: true, status: true },
});
if (!account || account.status !== 'ACTIVE') {
throw new ForbiddenException('HQ 账号不可用');
}
return account;
}
async resolveAccess(actorId: bigint): Promise<{
keys: HqPermissionKey[];
isSuperAdmin: boolean;
adminRole: HqAdminRoleValue;
}> {
const account = await this.loadActiveAccount(actorId);
const adminRole = account.adminRole as HqAdminRoleValue;
const userRows = await this.prisma.hqAccountPermission.findMany({
where: { hqAccountId: actorId },
select: { permissionKey: true, effect: true },
});
const grantKeys = userRows
.filter((r) => r.effect !== 'DENY')
.map((r) => r.permissionKey);
const denyKeys = userRows.filter((r) => r.effect === 'DENY').map((r) => r.permissionKey);
if (adminRole === 'SUPER_ADMIN') {
return {
isSuperAdmin: true,
adminRole,
keys: expandHqPermissionKeys([
...HQ_PERMISSION_CATALOG.map((p) => p.key),
...grantKeys,
]),
};
}
const roleRows = await this.prisma.hqRolePermission.findMany({
where: { adminRole },
select: { permissionKey: true },
});
const roleKeys =
roleRows.length > 0
? roleRows.map((r) => r.permissionKey)
: [...(HQ_ROLE_DEFAULT_PERMISSIONS[adminRole] ?? [])];
return {
isSuperAdmin: false,
adminRole,
keys: computeHqEffectivePermissionKeys(roleKeys, grantKeys, denyKeys),
};
}
async resolveEffectiveKeys(actorId: bigint): Promise<HqPermissionKey[]> {
const { keys } = await this.resolveAccess(actorId);
return keys;
}
async resolveCityScope(actorId: bigint): Promise<HqCityScope> {
const account = await this.loadActiveAccount(actorId);
if (account.adminRole === 'SUPER_ADMIN') return null;
const rows = await this.prisma.hqAccountCity.findMany({
where: { hqAccountId: actorId },
select: { cityId: true },
});
if (!rows.length) {
return account.adminRole === 'CITY_STORE_SERVICE' ? [] : null;
}
return rows.map((r) => r.cityId);
}
async assertStoreCityInScope(actorId: bigint, cityId: bigint) {
const scope = await this.resolveCityScope(actorId);
assertHqCityInScope(scope, cityId);
}
async assertStoreIdInScope(actorId: bigint, storeId: bigint) {
const store = await this.prisma.store.findUnique({
where: { id: storeId },
select: { cityId: true },
});
if (!store) {
throw new NotFoundException('门店不存在');
}
await this.assertStoreCityInScope(actorId, store.cityId);
}
}
@Injectable()
export class HqPermissionGuard implements CanActivate {
constructor(
private readonly resolver: HqPermissionsResolver,
private readonly reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const user = req.user as AuthUser | undefined;
if (!user || user.actorType !== 'HQ') {
throw new ForbiddenException('需要 HQ 权限');
}
const { keys, isSuperAdmin } = await this.resolver.resolveAccess(user.actorId);
req.hqPermissionKeys = keys;
const required =
this.reflector.getAllAndOverride<string[]>(HQ_PERMISSIONS_KEY, [
context.getHandler(),
context.getClass(),
]) ?? [];
if (!required.length) return true;
if (isSuperAdmin) return true;
if (required.includes('__any_system_settings__')) {
if (!hasAnySystemSettingsPermission(keys)) {
throw new ForbiddenException('无系统设置权限');
}
return true;
}
if (!required.some((k) => keys.includes(k as HqPermissionKey))) {
throw new ForbiddenException('权限不足');
}
return true;
}
}