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 { const { keys } = await this.resolveAccess(actorId); return keys; } async resolveCityScope(actorId: bigint): Promise { 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 { 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(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; } }