@@ -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 { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Layout, Menu, Typography, Button, Space } from 'antd';
|
import { Layout, Menu, Typography, Button, Space } from 'antd';
|
||||||
import type { MenuProps } from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
@@ -18,11 +18,14 @@ import {
|
|||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
AccountBookOutlined,
|
AccountBookOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import { hasAnySystemSettingsPermission } from '@dukang/shared-types';
|
||||||
import { clearAuth, request, type HqProfile } from '../lib/api';
|
import { clearAuth, request, type HqProfile } from '../lib/api';
|
||||||
import { bindAdminEllipsisTitle } from '../lib/ellipsis-title';
|
import { bindAdminEllipsisTitle } from '../lib/ellipsis-title';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider, Content } = Layout;
|
||||||
|
|
||||||
|
type MenuItem = NonNullable<MenuProps['items']>[number];
|
||||||
|
|
||||||
const MENU_ITEMS: MenuProps['items'] = [
|
const MENU_ITEMS: MenuProps['items'] = [
|
||||||
{ key: '/', icon: <DashboardOutlined />, label: '概览' },
|
{ key: '/', icon: <DashboardOutlined />, label: '概览' },
|
||||||
{ key: '/users', icon: <UserOutlined />, label: '用户' },
|
{ key: '/users', icon: <UserOutlined />, label: '用户' },
|
||||||
@@ -111,6 +114,75 @@ const MENU_ITEMS: MenuProps['items'] = [
|
|||||||
{ key: '/hq-accounts', icon: <SafetyOutlined />, label: 'HQ账户' },
|
{ key: '/hq-accounts', icon: <SafetyOutlined />, label: 'HQ账户' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function menuAllowed(key: string, permissionKeys: string[]): boolean {
|
||||||
|
const map: Record<string, string | 'system_settings_any'> = {
|
||||||
|
'/': '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() {
|
export default function AdminLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -138,6 +210,12 @@ export default function AdminLayout() {
|
|||||||
? '/promo-codes'
|
? '/promo-codes'
|
||||||
: location.pathname;
|
: 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 (
|
return (
|
||||||
<Layout style={{ height: '100vh', overflow: 'hidden' }}>
|
<Layout style={{ height: '100vh', overflow: 'hidden' }}>
|
||||||
<Sider
|
<Sider
|
||||||
@@ -156,8 +234,8 @@ export default function AdminLayout() {
|
|||||||
theme="dark"
|
theme="dark"
|
||||||
mode="inline"
|
mode="inline"
|
||||||
selectedKeys={[selectedKey]}
|
selectedKeys={[selectedKey]}
|
||||||
defaultOpenKeys={['products-group', 'stores-group', 'partners-group', 'finance-group', 'benefit-group', 'logs-group', 'deliveries-group']}
|
defaultOpenKeys={[]}
|
||||||
items={MENU_ITEMS}
|
items={menuItems}
|
||||||
onClick={({ key }) => {
|
onClick={({ key }) => {
|
||||||
if (key.startsWith('/')) navigate(key);
|
if (key.startsWith('/')) navigate(key);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type HqProfile = {
|
|||||||
name: string;
|
name: string;
|
||||||
adminRole: string;
|
adminRole: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
permissionKeys?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getToken() {
|
export function getToken() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Col,
|
Col,
|
||||||
|
Divider,
|
||||||
Form,
|
Form,
|
||||||
Row,
|
Row,
|
||||||
Select,
|
Select,
|
||||||
@@ -33,6 +34,8 @@ type AccountPermRes = {
|
|||||||
|
|
||||||
const ROLE_LABELS = Object.fromEntries(HQ_ADMIN_ROLES.map((r) => [r.value, r.label]));
|
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({
|
function PermissionChecklist({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -49,13 +52,24 @@ function PermissionChecklist({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={(checked) => onChange(checked as string[])}
|
onChange={(checked) => onChange(checked as string[])}
|
||||||
>
|
>
|
||||||
|
{CATALOG_GROUPS.map((group) => {
|
||||||
|
const items = HQ_PERMISSION_CATALOG.filter((p) => (p.group ?? '其他') === group);
|
||||||
|
return (
|
||||||
|
<div key={group} style={{ marginBottom: 16 }}>
|
||||||
|
<Typography.Text strong style={{ display: 'block', marginBottom: 8 }}>
|
||||||
|
{group}
|
||||||
|
</Typography.Text>
|
||||||
<Row gutter={[8, 8]}>
|
<Row gutter={[8, 8]}>
|
||||||
{HQ_PERMISSION_CATALOG.map((item) => (
|
{items.map((item) => (
|
||||||
<Col key={item.key} span={8}>
|
<Col key={item.key} span={8}>
|
||||||
<Checkbox value={item.key}>{item.label}</Checkbox>
|
<Checkbox value={item.key}>{item.label}</Checkbox>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
|
<Divider style={{ margin: '12px 0 0' }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Checkbox.Group>
|
</Checkbox.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -159,6 +173,7 @@ export default function HqPermissionsPage() {
|
|||||||
<Typography.Title level={4}>权限分配</Typography.Title>
|
<Typography.Title level={4}>权限分配</Typography.Title>
|
||||||
<Typography.Paragraph type="secondary">
|
<Typography.Paragraph type="secondary">
|
||||||
按角色配置基础权限;按用户可追加专属权限。最终生效权限 = 角色权限 ∪ 用户权限(超级管理员始终拥有全部权限)。
|
按角色配置基础权限;按用户可追加专属权限。最终生效权限 = 角色权限 ∪ 用户权限(超级管理员始终拥有全部权限)。
|
||||||
|
「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -231,7 +246,7 @@ export default function HqPermissionsPage() {
|
|||||||
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === key);
|
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === key);
|
||||||
return (
|
return (
|
||||||
<Tag key={key} color="blue">
|
<Tag key={key} color="blue">
|
||||||
{item?.label || key}
|
{item?.group ? `${item.group}·${item.label}` : item?.label || key}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -244,7 +259,11 @@ export default function HqPermissionsPage() {
|
|||||||
<span>合并生效:</span>
|
<span>合并生效:</span>
|
||||||
{previewEffectiveKeys.map((key) => {
|
{previewEffectiveKeys.map((key) => {
|
||||||
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey));
|
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey));
|
||||||
return <Tag key={key}>{item?.label || key}</Tag>;
|
return (
|
||||||
|
<Tag key={key}>
|
||||||
|
{item?.group ? `${item.group}·${item.label}` : item?.label || key}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</Space>
|
</Space>
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { MockSmsCodeItem, SystemConfigFieldMeta, SystemConfigFormResponse } from '@dukang/shared-types';
|
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';
|
import { ConfigImageField, ConfigImageListField } from '../components/ConfigMediaFields';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
@@ -161,6 +161,7 @@ export default function SystemSettingsPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [form] = Form.useForm<Record<string, string>>();
|
const [form] = Form.useForm<Record<string, string>>();
|
||||||
const [meta, setMeta] = useState<SystemConfigFormResponse | null>(null);
|
const [meta, setMeta] = useState<SystemConfigFormResponse | null>(null);
|
||||||
|
const [profile, setProfile] = useState<HqProfile | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [syncing, setSyncing] = useState(false);
|
||||||
@@ -195,6 +196,7 @@ export default function SystemSettingsPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
|
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -335,10 +337,14 @@ export default function SystemSettingsPage() {
|
|||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</div>
|
</div>
|
||||||
<Space>
|
<Space>
|
||||||
|
{profile?.adminRole === 'SUPER_ADMIN' ? (
|
||||||
|
<>
|
||||||
<Button onClick={() => void onImportEnv()}>从环境导入</Button>
|
<Button onClick={() => void onImportEnv()}>从环境导入</Button>
|
||||||
<Button loading={syncing} onClick={() => void onSyncEnv()}>
|
<Button loading={syncing} onClick={() => void onSyncEnv()}>
|
||||||
同步到 env 文件
|
同步到 env 文件
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -363,7 +369,7 @@ export default function SystemSettingsPage() {
|
|||||||
|
|
||||||
<Card loading={loading}>
|
<Card loading={loading}>
|
||||||
<Form form={form} layout="vertical" onValuesChange={() => setDirty(true)}>
|
<Form form={form} layout="vertical" onValuesChange={() => setDirty(true)}>
|
||||||
<Collapse defaultActiveKey={meta?.groups.map((g) => g.key)} items={collapseItems} />
|
<Collapse defaultActiveKey={[]} items={collapseItems} />
|
||||||
</Form>
|
</Form>
|
||||||
{meta?.updatedAt ? (
|
{meta?.updatedAt ? (
|
||||||
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 16 }}>
|
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 16 }}>
|
||||||
|
|||||||
@@ -1,24 +1,67 @@
|
|||||||
|
/** HQ 权限目录(权限分配页勾选源) */
|
||||||
export const HQ_PERMISSION_CATALOG = [
|
export const HQ_PERMISSION_CATALOG = [
|
||||||
{ key: 'dashboard', label: '概览' },
|
{ key: 'dashboard', label: '概览', group: '业务' },
|
||||||
{ key: 'users', label: '用户管理' },
|
{ key: 'users', label: '用户管理', group: '业务' },
|
||||||
{ key: 'wechat_bindings', label: '微信绑定' },
|
{ key: 'wechat_bindings', label: '微信绑定', group: '业务' },
|
||||||
{ key: 'products', label: '商品管理' },
|
{ key: 'products', label: '商品管理', group: '业务' },
|
||||||
{ key: 'orders', label: '订单管理' },
|
{ key: 'orders', label: '订单管理', group: '业务' },
|
||||||
{ key: 'stores', label: '门店管理' },
|
{ key: 'promo_codes', label: '推广码', group: '业务' },
|
||||||
{ key: 'partners', label: '开城管理' },
|
{ key: 'stores', label: '门店管理', group: '业务' },
|
||||||
{ key: 'benefit', label: '好客权益' },
|
{ key: 'partners', label: '开城管理', group: '业务' },
|
||||||
{ key: 'deliveries', label: '配送单' },
|
{ key: 'finance', label: '财务', group: '业务' },
|
||||||
{ key: 'tickets', label: '工单中心' },
|
{ key: 'benefit', label: '好客权益', group: '业务' },
|
||||||
{ key: 'invoices', label: '发票管理' },
|
{ key: 'deliveries', label: '配送单', group: '业务' },
|
||||||
{ key: 'resources', label: 'OSS 资源库' },
|
{ key: 'tickets', label: '工单中心', group: '业务' },
|
||||||
{ key: 'logs', label: '日志' },
|
{ key: 'invoices', label: '发票管理', group: '业务' },
|
||||||
{ key: 'hq_permissions', label: '权限分配' },
|
{ key: 'resources', label: 'OSS 资源库', group: '业务' },
|
||||||
{ key: 'hq_accounts', label: 'HQ 账户' },
|
{ key: 'logs', label: '日志', group: '业务' },
|
||||||
{ key: 'system_settings', label: '系统设置' },
|
{ 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;
|
] as const;
|
||||||
|
|
||||||
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
||||||
|
|
||||||
|
/** 系统配置 registry group → 权限 key */
|
||||||
|
export const SYSTEM_CONFIG_GROUP_PERMISSION: Record<string, HqPermissionKey> = {
|
||||||
|
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<string>(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 = [
|
export const HQ_ADMIN_ROLES = [
|
||||||
{ value: 'SUPER_ADMIN', label: '超级管理员' },
|
{ value: 'SUPER_ADMIN', label: '超级管理员' },
|
||||||
{ value: 'OPS', label: '运营' },
|
{ value: 'OPS', label: '运营' },
|
||||||
@@ -34,6 +77,7 @@ export const HQ_ROLE_DEFAULT_PERMISSIONS: Record<string, HqPermissionKey[]> = {
|
|||||||
'wechat_bindings',
|
'wechat_bindings',
|
||||||
'products',
|
'products',
|
||||||
'orders',
|
'orders',
|
||||||
|
'promo_codes',
|
||||||
'stores',
|
'stores',
|
||||||
'partners',
|
'partners',
|
||||||
'benefit',
|
'benefit',
|
||||||
@@ -42,7 +86,17 @@ export const HQ_ROLE_DEFAULT_PERMISSIONS: Record<string, HqPermissionKey[]> = {
|
|||||||
'invoices',
|
'invoices',
|
||||||
'resources',
|
'resources',
|
||||||
'logs',
|
'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'],
|
CUSTOMER_SERVICE: ['dashboard', 'users', 'orders', 'tickets', 'invoices', 'logs'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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());
|
return loadAppConfig(this.getMergedEnv());
|
||||||
}
|
}
|
||||||
|
|
||||||
async getForm(): Promise<SystemConfigFormResponse> {
|
async getForm(allowedGroups?: string[] | null): Promise<SystemConfigFormResponse> {
|
||||||
if (!this.tableReady) {
|
if (!this.tableReady) {
|
||||||
throw new Error('system_config 表未就绪,请在 server/dukang-api 执行 npx prisma db push');
|
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 rows = await this.prisma.systemConfig.findMany();
|
||||||
const dbMap = new Map(rows.map((r) => [r.configKey, r.value]));
|
const dbMap = new Map(rows.map((r) => [r.configKey, r.value]));
|
||||||
const values: Record<string, string> = {};
|
const values: Record<string, string> = {};
|
||||||
const configuredSecrets: string[] = [];
|
const configuredSecrets: string[] = [];
|
||||||
|
|
||||||
for (const field of SYSTEM_CONFIG_FIELDS) {
|
for (const field of fields) {
|
||||||
const fromDb = dbMap.get(field.key);
|
const fromDb = dbMap.get(field.key);
|
||||||
const fromEnv = process.env[field.key];
|
const fromEnv = process.env[field.key];
|
||||||
const raw = fromDb ?? fromEnv ?? '';
|
const raw = fromDb ?? fromEnv ?? '';
|
||||||
@@ -85,8 +92,8 @@ export class SystemConfigService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
groups: SYSTEM_CONFIG_GROUPS,
|
groups,
|
||||||
fields: SYSTEM_CONFIG_FIELDS,
|
fields,
|
||||||
values,
|
values,
|
||||||
configuredSecrets,
|
configuredSecrets,
|
||||||
envFilePath: resolveEnvFilePath(),
|
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[];
|
updatedKeys: string[];
|
||||||
requiresRestartKeys: string[];
|
requiresRestartKeys: string[];
|
||||||
}> {
|
}> {
|
||||||
const updatedKeys: string[] = [];
|
const updatedKeys: string[] = [];
|
||||||
const requiresRestartKeys: string[] = [];
|
const requiresRestartKeys: string[] = [];
|
||||||
const overlay: Record<string, string> = {};
|
const overlay: Record<string, string> = {};
|
||||||
|
const allowedGroupSet =
|
||||||
|
allowedGroups == null ? null : new Set(allowedGroups);
|
||||||
|
|
||||||
for (const [key, rawValue] of Object.entries(dto.values ?? {})) {
|
for (const [key, rawValue] of Object.entries(dto.values ?? {})) {
|
||||||
if (!SYSTEM_CONFIG_KEY_SET.has(key)) continue;
|
if (!SYSTEM_CONFIG_KEY_SET.has(key)) continue;
|
||||||
const meta = getSystemConfigField(key);
|
const meta = getSystemConfigField(key);
|
||||||
if (!meta) continue;
|
if (!meta) continue;
|
||||||
|
if (allowedGroupSet && !allowedGroupSet.has(meta.group)) continue;
|
||||||
|
|
||||||
let value = String(rawValue ?? '').trim();
|
let value = String(rawValue ?? '').trim();
|
||||||
if (meta.secret && (!value || value === SECRET_PLACEHOLDER)) {
|
if (meta.secret && (!value || value === SECRET_PLACEHOLDER)) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { verifyPassword } from '../../common/crypto/password.util';
|
|||||||
import { AnalyticsService } from '../analytics/analytics.service';
|
import { AnalyticsService } from '../analytics/analytics.service';
|
||||||
import { UserAddressService } from './user-address.service';
|
import { UserAddressService } from './user-address.service';
|
||||||
import { ResourceService } from '../common/resource.service';
|
import { ResourceService } from '../common/resource.service';
|
||||||
|
import { HqPermissionsResolver } from '../../common/guards/hq-permission.guard';
|
||||||
|
|
||||||
import type { User } from '@prisma/client';
|
import type { User } from '@prisma/client';
|
||||||
|
|
||||||
@@ -65,6 +66,7 @@ export class AuthService {
|
|||||||
private readonly smsCodeStore: SmsCodeStore,
|
private readonly smsCodeStore: SmsCodeStore,
|
||||||
private readonly userAddressService: UserAddressService,
|
private readonly userAddressService: UserAddressService,
|
||||||
@Inject(forwardRef(() => ResourceService)) private readonly resourceService: ResourceService,
|
@Inject(forwardRef(() => ResourceService)) private readonly resourceService: ResourceService,
|
||||||
|
private readonly hqPermissions: HqPermissionsResolver,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private assertMobilePhone(phone: string) {
|
private assertMobilePhone(phone: string) {
|
||||||
@@ -1128,7 +1130,9 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
if (actorType === 'HQ') {
|
if (actorType === 'HQ') {
|
||||||
const account = await this.prisma.hqAccount.findUnique({ where: { id: actorId } });
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ import { PartnerPrimaryGuard } from '../../common/guards/partner-primary.guard';
|
|||||||
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
|
import { PartnerPermissionGuard } from '../../common/guards/partner-permission.guard';
|
||||||
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
|
import { ShopStoreGuard } from '../../common/guards/shop-store.guard';
|
||||||
import { StoreMembershipService } from '../../common/guards/store-membership.service';
|
import { StoreMembershipService } from '../../common/guards/store-membership.service';
|
||||||
|
import {
|
||||||
|
HqPermissionGuard,
|
||||||
|
HqPermissionsResolver,
|
||||||
|
} from '../../common/guards/hq-permission.guard';
|
||||||
import { CommonModule } from '../common/common.module';
|
import { CommonModule } from '../common/common.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -59,6 +63,8 @@ import { CommonModule } from '../common/common.module';
|
|||||||
PartnerPrimaryGuard,
|
PartnerPrimaryGuard,
|
||||||
PartnerPermissionGuard,
|
PartnerPermissionGuard,
|
||||||
ShopStoreGuard,
|
ShopStoreGuard,
|
||||||
|
HqPermissionsResolver,
|
||||||
|
HqPermissionGuard,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AuthService,
|
AuthService,
|
||||||
@@ -74,6 +80,8 @@ import { CommonModule } from '../common/common.module';
|
|||||||
PartnerPrimaryGuard,
|
PartnerPrimaryGuard,
|
||||||
PartnerPermissionGuard,
|
PartnerPermissionGuard,
|
||||||
ShopStoreGuard,
|
ShopStoreGuard,
|
||||||
|
HqPermissionsResolver,
|
||||||
|
HqPermissionGuard,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class IamModule {}
|
export class IamModule {}
|
||||||
|
|||||||
@@ -2,12 +2,17 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm
|
|||||||
import {
|
import {
|
||||||
HQ_PERMISSION_CATALOG,
|
HQ_PERMISSION_CATALOG,
|
||||||
HQ_ROLE_DEFAULT_PERMISSIONS,
|
HQ_ROLE_DEFAULT_PERMISSIONS,
|
||||||
|
LEGACY_SYSTEM_SETTINGS_KEY,
|
||||||
|
expandHqPermissionKeys,
|
||||||
type HqPermissionKey,
|
type HqPermissionKey,
|
||||||
} from '@dukang/shared-types';
|
} from '@dukang/shared-types';
|
||||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
|
|
||||||
const VALID_PERMISSION_KEYS = new Set<string>(HQ_PERMISSION_CATALOG.map((p) => p.key));
|
const VALID_PERMISSION_KEYS = new Set<string>([
|
||||||
|
...HQ_PERMISSION_CATALOG.map((p) => p.key),
|
||||||
|
LEGACY_SYSTEM_SETTINGS_KEY,
|
||||||
|
]);
|
||||||
|
|
||||||
function assertPermissionKeys(keys: string[]) {
|
function assertPermissionKeys(keys: string[]) {
|
||||||
const invalid = keys.filter((key) => !VALID_PERMISSION_KEYS.has(key));
|
const invalid = keys.filter((key) => !VALID_PERMISSION_KEYS.has(key));
|
||||||
@@ -37,7 +42,7 @@ export class AdminHqPermissionsService {
|
|||||||
});
|
});
|
||||||
const permissionKeys =
|
const permissionKeys =
|
||||||
rows.length > 0
|
rows.length > 0
|
||||||
? rows.map((r) => r.permissionKey)
|
? expandHqPermissionKeys(rows.map((r) => r.permissionKey))
|
||||||
: [...(HQ_ROLE_DEFAULT_PERMISSIONS[role] ?? [])];
|
: [...(HQ_ROLE_DEFAULT_PERMISSIONS[role] ?? [])];
|
||||||
return { role, permissionKeys };
|
return { role, permissionKeys };
|
||||||
}
|
}
|
||||||
@@ -47,13 +52,14 @@ export class AdminHqPermissionsService {
|
|||||||
throw new BadRequestException('超级管理员拥有全部权限,无需配置');
|
throw new BadRequestException('超级管理员拥有全部权限,无需配置');
|
||||||
}
|
}
|
||||||
assertPermissionKeys(permissionKeys);
|
assertPermissionKeys(permissionKeys);
|
||||||
|
const normalized = expandHqPermissionKeys(permissionKeys);
|
||||||
const adminRole = role as 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE';
|
const adminRole = role as 'OPS' | 'FINANCE' | 'CUSTOMER_SERVICE';
|
||||||
await this.prisma.$transaction([
|
await this.prisma.$transaction([
|
||||||
this.prisma.hqRolePermission.deleteMany({ where: { adminRole } }),
|
this.prisma.hqRolePermission.deleteMany({ where: { adminRole } }),
|
||||||
...(permissionKeys.length
|
...(normalized.length
|
||||||
? [
|
? [
|
||||||
this.prisma.hqRolePermission.createMany({
|
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 },
|
select: { permissionKey: true },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
const userPermissionKeys = userPerms.map((p) => p.permissionKey);
|
const userPermissionKeys = expandHqPermissionKeys(userPerms.map((p) => p.permissionKey));
|
||||||
const effectivePermissionKeys = [
|
const effectivePermissionKeys = [
|
||||||
...new Set([...rolePerms.permissionKeys, ...userPermissionKeys]),
|
...new Set([...rolePerms.permissionKeys, ...userPermissionKeys]),
|
||||||
] as HqPermissionKey[];
|
] as HqPermissionKey[];
|
||||||
@@ -105,12 +111,13 @@ export class AdminHqPermissionsService {
|
|||||||
throw new BadRequestException('超级管理员拥有全部权限,无需配置');
|
throw new BadRequestException('超级管理员拥有全部权限,无需配置');
|
||||||
}
|
}
|
||||||
assertPermissionKeys(permissionKeys);
|
assertPermissionKeys(permissionKeys);
|
||||||
|
const normalized = expandHqPermissionKeys(permissionKeys);
|
||||||
await this.prisma.$transaction([
|
await this.prisma.$transaction([
|
||||||
this.prisma.hqAccountPermission.deleteMany({ where: { hqAccountId: accountId } }),
|
this.prisma.hqAccountPermission.deleteMany({ where: { hqAccountId: accountId } }),
|
||||||
...(permissionKeys.length
|
...(normalized.length
|
||||||
? [
|
? [
|
||||||
this.prisma.hqAccountPermission.createMany({
|
this.prisma.hqAccountPermission.createMany({
|
||||||
data: permissionKeys.map((permissionKey) => ({ hqAccountId: accountId, permissionKey })),
|
data: normalized.map((permissionKey) => ({ hqAccountId: accountId, permissionKey })),
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
@@ -1,22 +1,41 @@
|
|||||||
import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common';
|
||||||
import type { SystemConfigUpdateRequest } from '@dukang/shared-types';
|
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 { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||||
import { SuperAdminGuard } from '../../common/guards/super-admin.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 { HqOperation } from '../../common/hq-operation/hq-operation.decorator';
|
||||||
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
import { HqOperationAction } from '../../common/hq-operation/hq-operation.constants';
|
||||||
import { SystemConfigService } from '../../common/system-config/system-config.service';
|
import { SystemConfigService } from '../../common/system-config/system-config.service';
|
||||||
|
|
||||||
@Controller('admin/system-config')
|
@Controller('admin/system-config')
|
||||||
@UseGuards(HqAuthGuard, SuperAdminGuard)
|
@UseGuards(HqAuthGuard, HqPermissionGuard)
|
||||||
export class AdminSystemConfigController {
|
export class AdminSystemConfigController {
|
||||||
constructor(private readonly systemConfig: SystemConfigService) {}
|
constructor(
|
||||||
|
private readonly systemConfig: SystemConfigService,
|
||||||
|
private readonly permissions: HqPermissionsResolver,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
getForm() {
|
@RequireAnySystemSettings()
|
||||||
return this.systemConfig.getForm();
|
async getForm(@CurrentUser() user: AuthUser) {
|
||||||
|
const keys = await this.permissions.resolveEffectiveKeys(user.actorId);
|
||||||
|
const allowedGroups = allowedConfigGroups(keys);
|
||||||
|
return this.systemConfig.getForm(allowedGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put()
|
@Put()
|
||||||
|
@RequireAnySystemSettings()
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
action: HqOperationAction.SYSTEM_CONFIG_UPDATE,
|
action: HqOperationAction.SYSTEM_CONFIG_UPDATE,
|
||||||
refType: 'SYSTEM_CONFIG',
|
refType: 'SYSTEM_CONFIG',
|
||||||
@@ -24,11 +43,14 @@ export class AdminSystemConfigController {
|
|||||||
batch: true,
|
batch: true,
|
||||||
includeBody: true,
|
includeBody: true,
|
||||||
})
|
})
|
||||||
update(@Body() dto: SystemConfigUpdateRequest) {
|
async update(@CurrentUser() user: AuthUser, @Body() dto: SystemConfigUpdateRequest) {
|
||||||
return this.systemConfig.update(dto);
|
const keys = await this.permissions.resolveEffectiveKeys(user.actorId);
|
||||||
|
const allowedGroups = allowedConfigGroups(keys);
|
||||||
|
return this.systemConfig.update(dto, allowedGroups);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('sync-env')
|
@Post('sync-env')
|
||||||
|
@UseGuards(SuperAdminGuard)
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
action: HqOperationAction.SYSTEM_CONFIG_SYNC_ENV,
|
action: HqOperationAction.SYSTEM_CONFIG_SYNC_ENV,
|
||||||
refType: 'SYSTEM_CONFIG',
|
refType: 'SYSTEM_CONFIG',
|
||||||
@@ -39,6 +61,7 @@ export class AdminSystemConfigController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('import-env')
|
@Post('import-env')
|
||||||
|
@UseGuards(SuperAdminGuard)
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
action: HqOperationAction.SYSTEM_CONFIG_IMPORT_ENV,
|
action: HqOperationAction.SYSTEM_CONFIG_IMPORT_ENV,
|
||||||
refType: 'SYSTEM_CONFIG',
|
refType: 'SYSTEM_CONFIG',
|
||||||
@@ -48,3 +71,12 @@ export class AdminSystemConfigController {
|
|||||||
return this.systemConfig.importFromProcessEnv();
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user