@@ -0,0 +1,99 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
SetMetadata,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import {
|
||||
HQ_PERMISSION_CATALOG,
|
||||
HQ_ROLE_DEFAULT_PERMISSIONS,
|
||||
expandHqPermissionKeys,
|
||||
hasAnySystemSettingsPermission,
|
||||
type HqPermissionKey,
|
||||
} from '@dukang/shared-types';
|
||||
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__']);
|
||||
|
||||
@Injectable()
|
||||
export class HqPermissionsResolver {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async resolveEffectiveKeys(actorId: bigint): Promise<HqPermissionKey[]> {
|
||||
const account = await this.prisma.hqAccount.findUnique({
|
||||
where: { id: actorId },
|
||||
select: { adminRole: true, status: true },
|
||||
});
|
||||
if (!account || account.status !== 'ACTIVE') {
|
||||
throw new ForbiddenException('HQ 账号不可用');
|
||||
}
|
||||
if (account.adminRole === 'SUPER_ADMIN') {
|
||||
return HQ_PERMISSION_CATALOG.map((p) => p.key);
|
||||
}
|
||||
|
||||
const [roleRows, userRows] = await Promise.all([
|
||||
this.prisma.hqRolePermission.findMany({
|
||||
where: { adminRole: account.adminRole },
|
||||
select: { permissionKey: true },
|
||||
}),
|
||||
this.prisma.hqAccountPermission.findMany({
|
||||
where: { hqAccountId: actorId },
|
||||
select: { permissionKey: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
const roleKeys =
|
||||
roleRows.length > 0
|
||||
? roleRows.map((r) => r.permissionKey)
|
||||
: [...(HQ_ROLE_DEFAULT_PERMISSIONS[account.adminRole] ?? [])];
|
||||
|
||||
return expandHqPermissionKeys([
|
||||
...roleKeys,
|
||||
...userRows.map((r) => r.permissionKey),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@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 = await this.resolver.resolveEffectiveKeys(user.actorId);
|
||||
req.hqPermissionKeys = keys;
|
||||
|
||||
const required =
|
||||
this.reflector.getAllAndOverride<string[]>(HQ_PERMISSIONS_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]) ?? [];
|
||||
|
||||
if (!required.length) 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;
|
||||
}
|
||||
}
|
||||
@@ -63,16 +63,23 @@ export class SystemConfigService implements OnModuleInit {
|
||||
return loadAppConfig(this.getMergedEnv());
|
||||
}
|
||||
|
||||
async getForm(): Promise<SystemConfigFormResponse> {
|
||||
async getForm(allowedGroups?: string[] | null): Promise<SystemConfigFormResponse> {
|
||||
if (!this.tableReady) {
|
||||
throw new Error('system_config 表未就绪,请在 server/dukang-api 执行 npx prisma db push');
|
||||
}
|
||||
const groups =
|
||||
allowedGroups == null
|
||||
? SYSTEM_CONFIG_GROUPS
|
||||
: SYSTEM_CONFIG_GROUPS.filter((g) => allowedGroups.includes(g.key));
|
||||
const allowedGroupSet = new Set(groups.map((g) => g.key));
|
||||
const fields = SYSTEM_CONFIG_FIELDS.filter((f) => allowedGroupSet.has(f.group));
|
||||
|
||||
const rows = await this.prisma.systemConfig.findMany();
|
||||
const dbMap = new Map(rows.map((r) => [r.configKey, r.value]));
|
||||
const values: Record<string, string> = {};
|
||||
const configuredSecrets: string[] = [];
|
||||
|
||||
for (const field of SYSTEM_CONFIG_FIELDS) {
|
||||
for (const field of fields) {
|
||||
const fromDb = dbMap.get(field.key);
|
||||
const fromEnv = process.env[field.key];
|
||||
const raw = fromDb ?? fromEnv ?? '';
|
||||
@@ -85,8 +92,8 @@ export class SystemConfigService implements OnModuleInit {
|
||||
}
|
||||
|
||||
return {
|
||||
groups: SYSTEM_CONFIG_GROUPS,
|
||||
fields: SYSTEM_CONFIG_FIELDS,
|
||||
groups,
|
||||
fields,
|
||||
values,
|
||||
configuredSecrets,
|
||||
envFilePath: resolveEnvFilePath(),
|
||||
@@ -95,18 +102,24 @@ export class SystemConfigService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
async update(dto: SystemConfigUpdateRequest): Promise<{
|
||||
async update(
|
||||
dto: SystemConfigUpdateRequest,
|
||||
allowedGroups?: string[] | null,
|
||||
): Promise<{
|
||||
updatedKeys: string[];
|
||||
requiresRestartKeys: string[];
|
||||
}> {
|
||||
const updatedKeys: string[] = [];
|
||||
const requiresRestartKeys: string[] = [];
|
||||
const overlay: Record<string, string> = {};
|
||||
const allowedGroupSet =
|
||||
allowedGroups == null ? null : new Set(allowedGroups);
|
||||
|
||||
for (const [key, rawValue] of Object.entries(dto.values ?? {})) {
|
||||
if (!SYSTEM_CONFIG_KEY_SET.has(key)) continue;
|
||||
const meta = getSystemConfigField(key);
|
||||
if (!meta) continue;
|
||||
if (allowedGroupSet && !allowedGroupSet.has(meta.group)) continue;
|
||||
|
||||
let value = String(rawValue ?? '').trim();
|
||||
if (meta.secret && (!value || value === SECRET_PLACEHOLDER)) {
|
||||
|
||||
Reference in New Issue
Block a user