diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx index 5e4270a..b11b7d0 100644 --- a/apps/admin-web/src/layouts/AdminLayout.tsx +++ b/apps/admin-web/src/layouts/AdminLayout.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { Layout, Menu, Typography, Button, Space } from 'antd'; import type { MenuProps } from 'antd'; @@ -18,11 +18,14 @@ import { SettingOutlined, AccountBookOutlined, } from '@ant-design/icons'; +import { hasAnySystemSettingsPermission } from '@dukang/shared-types'; import { clearAuth, request, type HqProfile } from '../lib/api'; import { bindAdminEllipsisTitle } from '../lib/ellipsis-title'; const { Header, Sider, Content } = Layout; +type MenuItem = NonNullable[number]; + const MENU_ITEMS: MenuProps['items'] = [ { key: '/', icon: , label: '概览' }, { key: '/users', icon: , label: '用户' }, @@ -111,6 +114,75 @@ const MENU_ITEMS: MenuProps['items'] = [ { key: '/hq-accounts', icon: , label: 'HQ账户' }, ]; +function menuAllowed(key: string, permissionKeys: string[]): boolean { + const map: Record = { + '/': 'dashboard', + '/users': 'users', + '/wechat-bindings': 'wechat_bindings', + 'products-group': 'products', + '/products': 'products', + '/product-detail-templates': 'products', + '/orders': 'orders', + '/promo-codes': 'promo_codes', + 'stores-group': 'stores', + '/stores': 'stores', + '/store-categories': 'stores', + '/store-accounts': 'stores', + '/store-media': 'stores', + 'partners-group': 'partners', + '/cities': 'partners', + '/city-partners': 'partners', + '/city-warehouses': 'partners', + '/fulfillment-providers': 'partners', + 'finance-group': 'finance', + '/finance/store-bills': 'finance', + '/finance/partner-bills': 'finance', + '/finance/winery-bills': 'finance', + 'benefit-group': 'benefit', + '/benefit/coupons': 'benefit', + '/benefit/ledgers': 'benefit', + '/redeem-records': 'benefit', + '/redeem-pending': 'benefit', + '/redeem/debug': 'benefit', + 'deliveries-group': 'deliveries', + '/deliveries': 'deliveries', + '/deliveries/xiaofeixia': 'deliveries', + '/tickets': 'tickets', + '/invoices': 'invoices', + '/resources': 'resources', + 'logs-group': 'logs', + '/logs/users': 'logs', + '/logs/stores': 'logs', + '/logs/partners': 'logs', + '/logs/hq': 'logs', + '/logs/third-party': 'logs', + '/hq-permissions': 'hq_permissions', + '/system-settings': 'system_settings_any', + '/hq-accounts': 'hq_accounts', + }; + const need = map[key]; + if (!need) return true; + if (need === 'system_settings_any') return hasAnySystemSettingsPermission(permissionKeys); + return permissionKeys.includes(need); +} + +function filterMenuItems(items: MenuProps['items'], permissionKeys: string[]): MenuProps['items'] { + if (!items) return items; + return items + .map((item) => { + if (!item || typeof item !== 'object' || !('key' in item)) return item; + const key = String(item.key); + if ('children' in item && Array.isArray(item.children)) { + if (!menuAllowed(key, permissionKeys)) return null; + const children = filterMenuItems(item.children as MenuProps['items'], permissionKeys); + if (!children?.length) return null; + return { ...item, children } as MenuItem; + } + return menuAllowed(key, permissionKeys) ? item : null; + }) + .filter(Boolean) as MenuProps['items']; +} + export default function AdminLayout() { const navigate = useNavigate(); const location = useLocation(); @@ -138,6 +210,12 @@ export default function AdminLayout() { ? '/promo-codes' : location.pathname; + const menuItems = useMemo(() => { + if (!profile) return MENU_ITEMS; + if (profile.adminRole === 'SUPER_ADMIN') return MENU_ITEMS; + return filterMenuItems(MENU_ITEMS, profile.permissionKeys ?? []); + }, [profile]); + return ( { if (key.startsWith('/')) navigate(key); }} diff --git a/apps/admin-web/src/lib/api.ts b/apps/admin-web/src/lib/api.ts index 7950ddb..9b3c91c 100644 --- a/apps/admin-web/src/lib/api.ts +++ b/apps/admin-web/src/lib/api.ts @@ -7,6 +7,7 @@ export type HqProfile = { name: string; adminRole: string; status: string; + permissionKeys?: string[]; }; export function getToken() { diff --git a/apps/admin-web/src/pages/HqPermissionsPage.tsx b/apps/admin-web/src/pages/HqPermissionsPage.tsx index 11ae3ed..a131af9 100644 --- a/apps/admin-web/src/pages/HqPermissionsPage.tsx +++ b/apps/admin-web/src/pages/HqPermissionsPage.tsx @@ -5,6 +5,7 @@ import { Card, Checkbox, Col, + Divider, Form, Row, Select, @@ -33,6 +34,8 @@ type AccountPermRes = { const ROLE_LABELS = Object.fromEntries(HQ_ADMIN_ROLES.map((r) => [r.value, r.label])); +const CATALOG_GROUPS = [...new Set(HQ_PERMISSION_CATALOG.map((p) => p.group ?? '其他'))]; + function PermissionChecklist({ value, onChange, @@ -49,13 +52,24 @@ function PermissionChecklist({ disabled={disabled} onChange={(checked) => onChange(checked as string[])} > - - {HQ_PERMISSION_CATALOG.map((item) => ( - - {item.label} - - ))} - + {CATALOG_GROUPS.map((group) => { + const items = HQ_PERMISSION_CATALOG.filter((p) => (p.group ?? '其他') === group); + return ( +
+ + {group} + + + {items.map((item) => ( + + {item.label} + + ))} + + +
+ ); + })} ); } @@ -159,6 +173,7 @@ export default function HqPermissionsPage() { 权限分配 按角色配置基础权限;按用户可追加专属权限。最终生效权限 = 角色权限 ∪ 用户权限(超级管理员始终拥有全部权限)。 + 「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单。 p.key === key); return ( - {item?.label || key} + {item?.group ? `${item.group}·${item.label}` : item?.label || key} ); })} @@ -244,7 +259,11 @@ export default function HqPermissionsPage() { 合并生效: {previewEffectiveKeys.map((key) => { const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey)); - return {item?.label || key}; + return ( + + {item?.group ? `${item.group}·${item.label}` : item?.label || key} + + ); })}
diff --git a/apps/admin-web/src/pages/SystemSettingsPage.tsx b/apps/admin-web/src/pages/SystemSettingsPage.tsx index bace8c4..a4ed925 100644 --- a/apps/admin-web/src/pages/SystemSettingsPage.tsx +++ b/apps/admin-web/src/pages/SystemSettingsPage.tsx @@ -17,7 +17,7 @@ import { message, } from 'antd'; import type { MockSmsCodeItem, SystemConfigFieldMeta, SystemConfigFormResponse } from '@dukang/shared-types'; -import { request } from '../lib/api'; +import { request, type HqProfile } from '../lib/api'; import { ConfigImageField, ConfigImageListField } from '../components/ConfigMediaFields'; const { TextArea } = Input; @@ -161,6 +161,7 @@ export default function SystemSettingsPage() { const navigate = useNavigate(); const [form] = Form.useForm>(); const [meta, setMeta] = useState(null); + const [profile, setProfile] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [syncing, setSyncing] = useState(false); @@ -195,6 +196,7 @@ export default function SystemSettingsPage() { useEffect(() => { void load(); + request('/admin/auth/me').then(setProfile).catch(() => {}); }, []); useEffect(() => { @@ -335,10 +337,14 @@ export default function SystemSettingsPage() {
- - + {profile?.adminRole === 'SUPER_ADMIN' ? ( + <> + + + + ) : null} @@ -363,7 +369,7 @@ export default function SystemSettingsPage() {
setDirty(true)}> - g.key)} items={collapseItems} /> + {meta?.updatedAt ? ( diff --git a/packages/shared-types/src/hq-permissions.ts b/packages/shared-types/src/hq-permissions.ts index 8892518..60312f8 100644 --- a/packages/shared-types/src/hq-permissions.ts +++ b/packages/shared-types/src/hq-permissions.ts @@ -1,24 +1,67 @@ +/** HQ 权限目录(权限分配页勾选源) */ export const HQ_PERMISSION_CATALOG = [ - { key: 'dashboard', label: '概览' }, - { key: 'users', label: '用户管理' }, - { key: 'wechat_bindings', label: '微信绑定' }, - { key: 'products', label: '商品管理' }, - { key: 'orders', label: '订单管理' }, - { key: 'stores', label: '门店管理' }, - { key: 'partners', label: '开城管理' }, - { key: 'benefit', label: '好客权益' }, - { key: 'deliveries', label: '配送单' }, - { key: 'tickets', label: '工单中心' }, - { key: 'invoices', label: '发票管理' }, - { key: 'resources', label: 'OSS 资源库' }, - { key: 'logs', label: '日志' }, - { key: 'hq_permissions', label: '权限分配' }, - { key: 'hq_accounts', label: 'HQ 账户' }, - { key: 'system_settings', label: '系统设置' }, + { key: 'dashboard', label: '概览', group: '业务' }, + { key: 'users', label: '用户管理', group: '业务' }, + { key: 'wechat_bindings', label: '微信绑定', group: '业务' }, + { key: 'products', label: '商品管理', group: '业务' }, + { key: 'orders', label: '订单管理', group: '业务' }, + { key: 'promo_codes', label: '推广码', group: '业务' }, + { key: 'stores', label: '门店管理', group: '业务' }, + { key: 'partners', label: '开城管理', group: '业务' }, + { key: 'finance', label: '财务', group: '业务' }, + { key: 'benefit', label: '好客权益', group: '业务' }, + { key: 'deliveries', label: '配送单', group: '业务' }, + { key: 'tickets', label: '工单中心', group: '业务' }, + { key: 'invoices', label: '发票管理', group: '业务' }, + { key: 'resources', label: 'OSS 资源库', group: '业务' }, + { key: 'logs', label: '日志', group: '业务' }, + { key: 'hq_permissions', label: '权限分配', group: '管理' }, + { key: 'hq_accounts', label: 'HQ 账户', group: '管理' }, + { key: 'system_settings_feature', label: '功能开关', group: '系统设置' }, + { key: 'system_settings_sms', label: '短信', group: '系统设置' }, + { key: 'system_settings_wechat', label: '微信', group: '系统设置' }, + { key: 'system_settings_wechat_mini', label: '微信小程序', group: '系统设置' }, + { key: 'system_settings_oss', label: '对象存储 OSS', group: '系统设置' }, + { key: 'system_settings_app', label: '应用链接', group: '系统设置' }, + { key: 'system_settings_deploy', label: '发布部署', group: '系统设置' }, ] as const; export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key']; +/** 系统配置 registry group → 权限 key */ +export const SYSTEM_CONFIG_GROUP_PERMISSION: Record = { + feature: 'system_settings_feature', + sms: 'system_settings_sms', + wechat: 'system_settings_wechat', + wechat_mini: 'system_settings_wechat_mini', + oss: 'system_settings_oss', + app: 'system_settings_app', + deploy: 'system_settings_deploy', +}; + +export const SYSTEM_SETTINGS_PERMISSION_KEYS = Object.values( + SYSTEM_CONFIG_GROUP_PERMISSION, +) as HqPermissionKey[]; + +/** 旧版单一 system_settings 权限:视为拥有全部系统设置分组(兼容存量角色配置) */ +export const LEGACY_SYSTEM_SETTINGS_KEY = 'system_settings'; + +export function expandHqPermissionKeys(keys: string[]): HqPermissionKey[] { + const set = new Set(keys); + if (set.has(LEGACY_SYSTEM_SETTINGS_KEY)) { + for (const k of SYSTEM_SETTINGS_PERMISSION_KEYS) set.add(k); + set.delete(LEGACY_SYSTEM_SETTINGS_KEY); + } + return [...set].filter((k): k is HqPermissionKey => + HQ_PERMISSION_CATALOG.some((p) => p.key === k), + ); +} + +export function hasAnySystemSettingsPermission(keys: string[]): boolean { + const expanded = expandHqPermissionKeys(keys); + return SYSTEM_SETTINGS_PERMISSION_KEYS.some((k) => expanded.includes(k)); +} + export const HQ_ADMIN_ROLES = [ { value: 'SUPER_ADMIN', label: '超级管理员' }, { value: 'OPS', label: '运营' }, @@ -34,6 +77,7 @@ export const HQ_ROLE_DEFAULT_PERMISSIONS: Record = { 'wechat_bindings', 'products', 'orders', + 'promo_codes', 'stores', 'partners', 'benefit', @@ -42,7 +86,17 @@ export const HQ_ROLE_DEFAULT_PERMISSIONS: Record = { 'invoices', 'resources', 'logs', + 'system_settings_wechat_mini', + ], + FINANCE: [ + 'dashboard', + 'orders', + 'stores', + 'partners', + 'finance', + 'benefit', + 'invoices', + 'logs', ], - FINANCE: ['dashboard', 'orders', 'stores', 'partners', 'benefit', 'invoices', 'logs'], CUSTOMER_SERVICE: ['dashboard', 'users', 'orders', 'tickets', 'invoices', 'logs'], }; diff --git a/server/dukang-api/src/common/guards/hq-permission.guard.ts b/server/dukang-api/src/common/guards/hq-permission.guard.ts new file mode 100644 index 0000000..8e7d4c2 --- /dev/null +++ b/server/dukang-api/src/common/guards/hq-permission.guard.ts @@ -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 { + 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 { + 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(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; + } +} diff --git a/server/dukang-api/src/common/system-config/system-config.service.ts b/server/dukang-api/src/common/system-config/system-config.service.ts index f732a8a..7c29c5a 100644 --- a/server/dukang-api/src/common/system-config/system-config.service.ts +++ b/server/dukang-api/src/common/system-config/system-config.service.ts @@ -63,16 +63,23 @@ export class SystemConfigService implements OnModuleInit { return loadAppConfig(this.getMergedEnv()); } - async getForm(): Promise { + async getForm(allowedGroups?: string[] | null): Promise { 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 = {}; 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 = {}; + 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)) { diff --git a/server/dukang-api/src/modules/iam/auth.service.ts b/server/dukang-api/src/modules/iam/auth.service.ts index cc84ee0..567dde6 100644 --- a/server/dukang-api/src/modules/iam/auth.service.ts +++ b/server/dukang-api/src/modules/iam/auth.service.ts @@ -24,6 +24,7 @@ import { verifyPassword } from '../../common/crypto/password.util'; import { AnalyticsService } from '../analytics/analytics.service'; import { UserAddressService } from './user-address.service'; import { ResourceService } from '../common/resource.service'; +import { HqPermissionsResolver } from '../../common/guards/hq-permission.guard'; import type { User } from '@prisma/client'; @@ -65,6 +66,7 @@ export class AuthService { private readonly smsCodeStore: SmsCodeStore, private readonly userAddressService: UserAddressService, @Inject(forwardRef(() => ResourceService)) private readonly resourceService: ResourceService, + private readonly hqPermissions: HqPermissionsResolver, ) {} private assertMobilePhone(phone: string) { @@ -1128,7 +1130,9 @@ export class AuthService { } if (actorType === 'HQ') { const account = await this.prisma.hqAccount.findUnique({ where: { id: actorId } }); - return serializeBigInt(account); + if (!account) return null; + const permissionKeys = await this.hqPermissions.resolveEffectiveKeys(actorId); + return serializeBigInt({ ...account, permissionKeys }); } return null; } diff --git a/server/dukang-api/src/modules/iam/iam.module.ts b/server/dukang-api/src/modules/iam/iam.module.ts index 880a6a2..5b74df9 100644 --- a/server/dukang-api/src/modules/iam/iam.module.ts +++ b/server/dukang-api/src/modules/iam/iam.module.ts @@ -24,6 +24,10 @@ import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard'; import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard'; import { ShopStoreGuard } from '../../common/guards/shop-store.guard'; import { StoreMembershipService } from '../../common/guards/store-membership.service'; +import { + HqPermissionGuard, + HqPermissionsResolver, +} from '../../common/guards/hq-permission.guard'; import { CommonModule } from '../common/common.module'; @Module({ @@ -59,6 +63,8 @@ import { CommonModule } from '../common/common.module'; PartnerPrimaryGuard, PartnerPermissionGuard, ShopStoreGuard, + HqPermissionsResolver, + HqPermissionGuard, ], exports: [ AuthService, @@ -74,6 +80,8 @@ import { CommonModule } from '../common/common.module'; PartnerPrimaryGuard, PartnerPermissionGuard, ShopStoreGuard, + HqPermissionsResolver, + HqPermissionGuard, ], }) export class IamModule {} diff --git a/server/dukang-api/src/modules/ops/admin-hq-permissions.service.ts b/server/dukang-api/src/modules/ops/admin-hq-permissions.service.ts index 452162c..5427992 100644 --- a/server/dukang-api/src/modules/ops/admin-hq-permissions.service.ts +++ b/server/dukang-api/src/modules/ops/admin-hq-permissions.service.ts @@ -2,12 +2,17 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm import { HQ_PERMISSION_CATALOG, HQ_ROLE_DEFAULT_PERMISSIONS, + LEGACY_SYSTEM_SETTINGS_KEY, + expandHqPermissionKeys, type HqPermissionKey, } from '@dukang/shared-types'; import { PrismaService } from '../../common/prisma/prisma.module'; import { serializeBigInt } from '../../common/decorators/current-user.decorator'; -const VALID_PERMISSION_KEYS = new Set(HQ_PERMISSION_CATALOG.map((p) => p.key)); +const VALID_PERMISSION_KEYS = new Set([ + ...HQ_PERMISSION_CATALOG.map((p) => p.key), + LEGACY_SYSTEM_SETTINGS_KEY, +]); function assertPermissionKeys(keys: string[]) { const invalid = keys.filter((key) => !VALID_PERMISSION_KEYS.has(key)); @@ -37,7 +42,7 @@ export class AdminHqPermissionsService { }); const permissionKeys = rows.length > 0 - ? rows.map((r) => r.permissionKey) + ? expandHqPermissionKeys(rows.map((r) => r.permissionKey)) : [...(HQ_ROLE_DEFAULT_PERMISSIONS[role] ?? [])]; return { role, permissionKeys }; } @@ -47,13 +52,14 @@ export class AdminHqPermissionsService { throw new BadRequestException('超级管理员拥有全部权限,无需配置'); } assertPermissionKeys(permissionKeys); + const normalized = expandHqPermissionKeys(permissionKeys); const adminRole = role as 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE'; await this.prisma.$transaction([ this.prisma.hqRolePermission.deleteMany({ where: { adminRole } }), - ...(permissionKeys.length + ...(normalized.length ? [ this.prisma.hqRolePermission.createMany({ - data: permissionKeys.map((permissionKey) => ({ adminRole, permissionKey })), + data: normalized.map((permissionKey) => ({ adminRole, permissionKey })), }), ] : []), @@ -84,7 +90,7 @@ export class AdminHqPermissionsService { select: { permissionKey: true }, }), ]); - const userPermissionKeys = userPerms.map((p) => p.permissionKey); + const userPermissionKeys = expandHqPermissionKeys(userPerms.map((p) => p.permissionKey)); const effectivePermissionKeys = [ ...new Set([...rolePerms.permissionKeys, ...userPermissionKeys]), ] as HqPermissionKey[]; @@ -105,12 +111,13 @@ export class AdminHqPermissionsService { throw new BadRequestException('超级管理员拥有全部权限,无需配置'); } assertPermissionKeys(permissionKeys); + const normalized = expandHqPermissionKeys(permissionKeys); await this.prisma.$transaction([ this.prisma.hqAccountPermission.deleteMany({ where: { hqAccountId: accountId } }), - ...(permissionKeys.length + ...(normalized.length ? [ this.prisma.hqAccountPermission.createMany({ - data: permissionKeys.map((permissionKey) => ({ hqAccountId: accountId, permissionKey })), + data: normalized.map((permissionKey) => ({ hqAccountId: accountId, permissionKey })), }), ] : []), diff --git a/server/dukang-api/src/modules/ops/admin-system-config.controller.ts b/server/dukang-api/src/modules/ops/admin-system-config.controller.ts index b893192..3b334b3 100644 --- a/server/dukang-api/src/modules/ops/admin-system-config.controller.ts +++ b/server/dukang-api/src/modules/ops/admin-system-config.controller.ts @@ -1,22 +1,41 @@ import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common'; import type { SystemConfigUpdateRequest } from '@dukang/shared-types'; +import { + SYSTEM_CONFIG_GROUP_PERMISSION, + SYSTEM_SETTINGS_PERMISSION_KEYS, + type HqPermissionKey, +} from '@dukang/shared-types'; import { HqAuthGuard } from '../../common/guards/hq-auth.guard'; import { SuperAdminGuard } from '../../common/guards/super-admin.guard'; +import { + HqPermissionGuard, + HqPermissionsResolver, + RequireAnySystemSettings, +} from '../../common/guards/hq-permission.guard'; +import { CurrentUser } from '../../common/decorators/current-user.decorator'; +import type { AuthUser } from '../../common/guards/jwt-auth.guard'; import { HqOperation } from '../../common/hq-operation/hq-operation.decorator'; import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants'; import { SystemConfigService } from '../../common/system-config/system-config.service'; @Controller('admin/system-config') -@UseGuards(HqAuthGuard, SuperAdminGuard) +@UseGuards(HqAuthGuard, HqPermissionGuard) export class AdminSystemConfigController { - constructor(private readonly systemConfig: SystemConfigService) {} + constructor( + private readonly systemConfig: SystemConfigService, + private readonly permissions: HqPermissionsResolver, + ) {} @Get() - getForm() { - return this.systemConfig.getForm(); + @RequireAnySystemSettings() + async getForm(@CurrentUser() user: AuthUser) { + const keys = await this.permissions.resolveEffectiveKeys(user.actorId); + const allowedGroups = allowedConfigGroups(keys); + return this.systemConfig.getForm(allowedGroups); } @Put() + @RequireAnySystemSettings() @HqOperation({ action: HqOperationAction.SYSTEM_CONFIG_UPDATE, refType: 'SYSTEM_CONFIG', @@ -24,11 +43,14 @@ export class AdminSystemConfigController { batch: true, includeBody: true, }) - update(@Body() dto: SystemConfigUpdateRequest) { - return this.systemConfig.update(dto); + async update(@CurrentUser() user: AuthUser, @Body() dto: SystemConfigUpdateRequest) { + const keys = await this.permissions.resolveEffectiveKeys(user.actorId); + const allowedGroups = allowedConfigGroups(keys); + return this.systemConfig.update(dto, allowedGroups); } @Post('sync-env') + @UseGuards(SuperAdminGuard) @HqOperation({ action: HqOperationAction.SYSTEM_CONFIG_SYNC_ENV, refType: 'SYSTEM_CONFIG', @@ -39,6 +61,7 @@ export class AdminSystemConfigController { } @Post('import-env') + @UseGuards(SuperAdminGuard) @HqOperation({ action: HqOperationAction.SYSTEM_CONFIG_IMPORT_ENV, refType: 'SYSTEM_CONFIG', @@ -48,3 +71,12 @@ export class AdminSystemConfigController { return this.systemConfig.importFromProcessEnv(); } } + +function allowedConfigGroups(permissionKeys: HqPermissionKey[]): string[] | null { + if (SYSTEM_SETTINGS_PERMISSION_KEYS.every((k) => permissionKeys.includes(k))) { + return null; // 全部 + } + return Object.entries(SYSTEM_CONFIG_GROUP_PERMISSION) + .filter(([, perm]) => permissionKeys.includes(perm)) + .map(([group]) => group); +}