feat(admin): gate user/order/city delete behind assignable HQ permissions
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Add users_delete, orders_delete, cities_delete; default HQ (incl. super admin) has none until explicitly granted. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import { WAREHOUSE_MANAGER_LABELS, WarehouseManagerType } from '@dukang/shared-types';
|
import { WAREHOUSE_MANAGER_LABELS, WarehouseManagerType } from '@dukang/shared-types';
|
||||||
import { request, type Paginated } from '../lib/api';
|
import { request, type HqProfile, type Paginated } from '../lib/api';
|
||||||
import { parseProvinceCityCodes, type ParsedProvinceCity } from '../lib/china-region';
|
import { parseProvinceCityCodes, type ParsedProvinceCity } from '../lib/china-region';
|
||||||
import { ADMIN_OPTIONS_PAGE_SIZE, CITY_STATUS_LABELS, fmtTime } from '../lib/constants';
|
import { ADMIN_OPTIONS_PAGE_SIZE, CITY_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||||
import { useAdminList } from '../lib/useAdminList';
|
import { useAdminList } from '../lib/useAdminList';
|
||||||
@@ -122,6 +122,12 @@ export default function CitiesPage() {
|
|||||||
const [deleteTarget, setDeleteTarget] = useState<Row | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<Row | null>(null);
|
||||||
const [deletePreview, setDeletePreview] = useState<CityDeletePreview | null>(null);
|
const [deletePreview, setDeletePreview] = useState<CityDeletePreview | null>(null);
|
||||||
const [confirmName, setConfirmName] = useState('');
|
const [confirmName, setConfirmName] = useState('');
|
||||||
|
const [profile, setProfile] = useState<HqProfile | null>(null);
|
||||||
|
const canDeleteCities = (profile?.permissionKeys ?? []).includes('cities_delete');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loadPartners = useCallback(async (cityId: string) => {
|
const loadPartners = useCallback(async (cityId: string) => {
|
||||||
const res = await request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}&cityId=${cityId}`);
|
const res = await request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}&cityId=${cityId}`);
|
||||||
@@ -233,9 +239,11 @@ export default function CitiesPage() {
|
|||||||
<Button type="link" size="small" onClick={() => void openDetail(row)}>
|
<Button type="link" size="small" onClick={() => void openDetail(row)}>
|
||||||
管理
|
管理
|
||||||
</Button>
|
</Button>
|
||||||
|
{canDeleteCities ? (
|
||||||
<Button type="link" size="small" danger onClick={() => void openDelete(row)}>
|
<Button type="link" size="small" danger onClick={() => void openDelete(row)}>
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -356,6 +364,7 @@ export default function CitiesPage() {
|
|||||||
setDetail(refreshed);
|
setDetail(refreshed);
|
||||||
void reload();
|
void reload();
|
||||||
}}>保存</Button>
|
}}>保存</Button>
|
||||||
|
{canDeleteCities ? (
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
style={{ marginLeft: 8 }}
|
style={{ marginLeft: 8 }}
|
||||||
@@ -374,6 +383,7 @@ export default function CitiesPage() {
|
|||||||
>
|
>
|
||||||
删除城市
|
删除城市
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ export default function HqPermissionsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<Typography.Title level={4}>权限分配</Typography.Title>
|
<Typography.Title level={4}>权限分配</Typography.Title>
|
||||||
<Typography.Paragraph type="secondary">
|
<Typography.Paragraph type="secondary">
|
||||||
按角色配置基础权限;按用户可追加专属权限。最终生效权限 = 角色权限 ∪ 用户权限(超级管理员始终拥有全部权限)。
|
按角色配置基础权限;按用户可追加专属权限。最终生效权限 = 角色权限 ∪ 用户权限。
|
||||||
|
超级管理员默认拥有除「危险操作」外的全部权限;删除用户/订单/城市需在「按用户分配」中单独勾选(默认均无)。
|
||||||
「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单。
|
「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
|
||||||
@@ -198,7 +199,11 @@ export default function HqPermissionsPage() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
{role === 'SUPER_ADMIN' ? (
|
{role === 'SUPER_ADMIN' ? (
|
||||||
<Alert type="info" showIcon message="超级管理员拥有全部权限,无需配置" />
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message="超级管理员基础权限固定(不含危险操作)。删除用户/订单/城市请到「按用户分配」为具体账号勾选。"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<PermissionChecklist value={roleKeys} onChange={setRoleKeys} />
|
<PermissionChecklist value={roleKeys} onChange={setRoleKeys} />
|
||||||
@@ -227,9 +232,7 @@ export default function HqPermissionsPage() {
|
|||||||
value={accountId}
|
value={accountId}
|
||||||
onChange={setAccountId}
|
onChange={setAccountId}
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
options={accounts
|
options={accounts.map((a) => ({
|
||||||
.filter((a) => a.adminRole !== 'SUPER_ADMIN')
|
|
||||||
.map((a) => ({
|
|
||||||
value: a.id,
|
value: a.id,
|
||||||
label: `${a.name} · ${a.loginName || a.phone} · ${ROLE_LABELS[a.adminRole] || a.adminRole}`,
|
label: `${a.name} · ${a.loginName || a.phone} · ${ROLE_LABELS[a.adminRole] || a.adminRole}`,
|
||||||
}))}
|
}))}
|
||||||
@@ -252,7 +255,7 @@ export default function HqPermissionsPage() {
|
|||||||
})}
|
})}
|
||||||
</Space>
|
</Space>
|
||||||
<Typography.Paragraph type="secondary">
|
<Typography.Paragraph type="secondary">
|
||||||
下方勾选为用户专属追加权限(保存后与角色权限合并生效)。
|
下方勾选为用户专属追加权限(保存后与角色权限合并生效)。危险操作(删除用户/订单/城市)默认不授予,需在此勾选。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
<PermissionChecklist value={accountKeys} onChange={setAccountKeys} />
|
<PermissionChecklist value={accountKeys} onChange={setAccountKeys} />
|
||||||
<Space wrap style={{ marginTop: 12 }}>
|
<Space wrap style={{ marginTop: 12 }}>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
message,
|
message,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import { request, type AdminOrderItem, type AdminOrderRow, type Paginated } from '../lib/api';
|
import { request, type AdminOrderItem, type AdminOrderRow, type HqProfile, type Paginated } from '../lib/api';
|
||||||
import {
|
import {
|
||||||
ADMIN_OPTIONS_PAGE_SIZE,
|
ADMIN_OPTIONS_PAGE_SIZE,
|
||||||
DELIVERY_PROVIDER_LABELS,
|
DELIVERY_PROVIDER_LABELS,
|
||||||
@@ -154,6 +154,7 @@ export default function OrdersPage() {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [shipForm] = Form.useForm();
|
const [shipForm] = Form.useForm();
|
||||||
const [logisticsForm] = Form.useForm();
|
const [logisticsForm] = Form.useForm();
|
||||||
|
const [profile, setProfile] = useState<HqProfile | null>(null);
|
||||||
const [data, setData] = useState<Paginated<AdminOrderRow> | null>(null);
|
const [data, setData] = useState<Paginated<AdminOrderRow> | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@@ -174,12 +175,17 @@ export default function OrdersPage() {
|
|||||||
const [shipTarget, setShipTarget] = useState<OrderDetail | null>(null);
|
const [shipTarget, setShipTarget] = useState<OrderDetail | null>(null);
|
||||||
const [shipMode, setShipMode] = useState<ShipMode>('EXPRESS');
|
const [shipMode, setShipMode] = useState<ShipMode>('EXPRESS');
|
||||||
const [warehouses, setWarehouses] = useState<WarehouseOption[]>([]);
|
const [warehouses, setWarehouses] = useState<WarehouseOption[]>([]);
|
||||||
|
const canDeleteOrders = (profile?.permissionKeys ?? []).includes('orders_delete');
|
||||||
|
|
||||||
const selectedOrders = useMemo(
|
const selectedOrders = useMemo(
|
||||||
() => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)),
|
() => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)),
|
||||||
[data?.items, selectedRowKeys],
|
[data?.items, selectedRowKeys],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function openRedeemDetail(redeemId: string) {
|
async function openRedeemDetail(redeemId: string) {
|
||||||
setRedeemDetailLoading(true);
|
setRedeemDetailLoading(true);
|
||||||
setRedeemDrawerOpen(true);
|
setRedeemDrawerOpen(true);
|
||||||
@@ -454,6 +460,7 @@ export default function OrdersPage() {
|
|||||||
<div>
|
<div>
|
||||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||||||
<Typography.Title level={4} style={{ margin: 0 }}>订单监控</Typography.Title>
|
<Typography.Title level={4} style={{ margin: 0 }}>订单监控</Typography.Title>
|
||||||
|
{canDeleteOrders ? (
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
disabled={!selectedRowKeys.length}
|
disabled={!selectedRowKeys.length}
|
||||||
@@ -461,6 +468,7 @@ export default function OrdersPage() {
|
|||||||
>
|
>
|
||||||
批量删除{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
|
批量删除{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
|
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
|
||||||
<Form.Item name="orderNo" label="订单号">
|
<Form.Item name="orderNo" label="订单号">
|
||||||
@@ -504,10 +512,10 @@ export default function OrdersPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.items ?? []}
|
dataSource={data?.items ?? []}
|
||||||
scroll={{ x: 1500 }}
|
scroll={{ x: 1500 }}
|
||||||
rowSelection={{
|
rowSelection={canDeleteOrders ? {
|
||||||
selectedRowKeys,
|
selectedRowKeys,
|
||||||
onChange: (keys) => setSelectedRowKeys(keys as string[]),
|
onChange: (keys) => setSelectedRowKeys(keys as string[]),
|
||||||
}}
|
} : undefined}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import { USER_SOURCE_TYPE_LABELS, type UserSourceType } from '@dukang/shared-types';
|
import { USER_SOURCE_TYPE_LABELS, type UserSourceType } from '@dukang/shared-types';
|
||||||
import { request, type AdminUserRow, type Paginated } from '../lib/api';
|
import { request, type AdminUserRow, type HqProfile, type Paginated } from '../lib/api';
|
||||||
import { ORDER_STATUS_LABELS, fmtTime, maskPhone } from '../lib/constants';
|
import { ORDER_STATUS_LABELS, fmtTime, maskPhone } from '../lib/constants';
|
||||||
|
|
||||||
type UserOrderRow = {
|
type UserOrderRow = {
|
||||||
@@ -66,6 +66,7 @@ export default function UsersPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const [profile, setProfile] = useState<HqProfile | null>(null);
|
||||||
const [data, setData] = useState<Paginated<AdminUserRow> | null>(null);
|
const [data, setData] = useState<Paginated<AdminUserRow> | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@@ -82,6 +83,11 @@ export default function UsersPage() {
|
|||||||
const [batchPreviewLoading, setBatchPreviewLoading] = useState(false);
|
const [batchPreviewLoading, setBatchPreviewLoading] = useState(false);
|
||||||
const [batchDeleting, setBatchDeleting] = useState(false);
|
const [batchDeleting, setBatchDeleting] = useState(false);
|
||||||
const [batchRiskAck, setBatchRiskAck] = useState(false);
|
const [batchRiskAck, setBatchRiskAck] = useState(false);
|
||||||
|
const canDeleteUsers = (profile?.permissionKeys ?? []).includes('users_delete');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -309,6 +315,7 @@ export default function UsersPage() {
|
|||||||
<div>
|
<div>
|
||||||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||||||
<Typography.Title level={4} style={{ margin: 0 }}>用户监控</Typography.Title>
|
<Typography.Title level={4} style={{ margin: 0 }}>用户监控</Typography.Title>
|
||||||
|
{canDeleteUsers ? (
|
||||||
<Button
|
<Button
|
||||||
danger
|
danger
|
||||||
disabled={!selectedRowKeys.length}
|
disabled={!selectedRowKeys.length}
|
||||||
@@ -316,6 +323,7 @@ export default function UsersPage() {
|
|||||||
>
|
>
|
||||||
批量删除{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
|
批量删除{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
|
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
|
||||||
<Form.Item name="phone" label="手机号">
|
<Form.Item name="phone" label="手机号">
|
||||||
@@ -353,11 +361,11 @@ export default function UsersPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data?.items ?? []}
|
dataSource={data?.items ?? []}
|
||||||
scroll={{ x: 1500 }}
|
scroll={{ x: 1500 }}
|
||||||
rowSelection={{
|
rowSelection={canDeleteUsers ? {
|
||||||
selectedRowKeys,
|
selectedRowKeys,
|
||||||
preserveSelectedRowKeys: true,
|
preserveSelectedRowKeys: true,
|
||||||
onChange: (keys) => setSelectedRowKeys(keys as string[]),
|
onChange: (keys) => setSelectedRowKeys(keys as string[]),
|
||||||
}}
|
} : undefined}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -375,9 +383,9 @@ export default function UsersPage() {
|
|||||||
width={640}
|
width={640}
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
onClose={() => setDrawerOpen(false)}
|
onClose={() => setDrawerOpen(false)}
|
||||||
extra={detail && (
|
extra={detail && canDeleteUsers ? (
|
||||||
<Button danger onClick={openDeleteModal}>删除用户</Button>
|
<Button danger onClick={openDeleteModal}>删除用户</Button>
|
||||||
)}
|
) : undefined}
|
||||||
>
|
>
|
||||||
{detail && (
|
{detail && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export const HQ_PERMISSION_CATALOG = [
|
|||||||
{ key: 'knowledge_bases', label: '知识库', group: '业务' },
|
{ key: 'knowledge_bases', label: '知识库', group: '业务' },
|
||||||
{ key: 'resources', label: 'OSS 资源库', group: '业务' },
|
{ key: 'resources', label: 'OSS 资源库', group: '业务' },
|
||||||
{ key: 'logs', label: '日志', group: '业务' },
|
{ key: 'logs', label: '日志', group: '业务' },
|
||||||
|
{ key: 'users_delete', label: '删除用户', group: '危险操作' },
|
||||||
|
{ key: 'orders_delete', label: '删除订单', group: '危险操作' },
|
||||||
|
{ key: 'cities_delete', label: '删除城市', group: '危险操作' },
|
||||||
{ key: 'hq_permissions', label: '权限分配', group: '管理' },
|
{ key: 'hq_permissions', label: '权限分配', group: '管理' },
|
||||||
{ key: 'hq_accounts', label: 'HQ 账户', group: '管理' },
|
{ key: 'hq_accounts', label: 'HQ 账户', group: '管理' },
|
||||||
{ key: 'system_settings_feature', label: '功能开关', group: '系统设置' },
|
{ key: 'system_settings_feature', label: '功能开关', group: '系统设置' },
|
||||||
@@ -33,6 +36,21 @@ export const HQ_PERMISSION_CATALOG = [
|
|||||||
|
|
||||||
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
export type HqPermissionKey = (typeof HQ_PERMISSION_CATALOG)[number]['key'];
|
||||||
|
|
||||||
|
/** 危险操作:角色默认与超管自动权限均不含,需在权限分配中显式勾选 */
|
||||||
|
export const HQ_DANGEROUS_PERMISSION_KEYS = [
|
||||||
|
'users_delete',
|
||||||
|
'orders_delete',
|
||||||
|
'cities_delete',
|
||||||
|
] as const satisfies readonly HqPermissionKey[];
|
||||||
|
|
||||||
|
export function isHqDangerousPermission(key: string): boolean {
|
||||||
|
return (HQ_DANGEROUS_PERMISSION_KEYS as readonly string[]).includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hqBasePermissionKeys(): HqPermissionKey[] {
|
||||||
|
return HQ_PERMISSION_CATALOG.map((p) => p.key).filter((k) => !isHqDangerousPermission(k));
|
||||||
|
}
|
||||||
|
|
||||||
/** 系统配置 registry group → 权限 key */
|
/** 系统配置 registry group → 权限 key */
|
||||||
export const SYSTEM_CONFIG_GROUP_PERMISSION: Record<string, HqPermissionKey> = {
|
export const SYSTEM_CONFIG_GROUP_PERMISSION: Record<string, HqPermissionKey> = {
|
||||||
feature: 'system_settings_feature',
|
feature: 'system_settings_feature',
|
||||||
@@ -76,7 +94,7 @@ export const HQ_ADMIN_ROLES = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const HQ_ROLE_DEFAULT_PERMISSIONS: Record<string, HqPermissionKey[]> = {
|
export const HQ_ROLE_DEFAULT_PERMISSIONS: Record<string, HqPermissionKey[]> = {
|
||||||
SUPER_ADMIN: HQ_PERMISSION_CATALOG.map((p) => p.key),
|
SUPER_ADMIN: hqBasePermissionKeys(),
|
||||||
OPS: [
|
OPS: [
|
||||||
'dashboard',
|
'dashboard',
|
||||||
'users',
|
'users',
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import {
|
import {
|
||||||
HQ_PERMISSION_CATALOG,
|
|
||||||
HQ_ROLE_DEFAULT_PERMISSIONS,
|
HQ_ROLE_DEFAULT_PERMISSIONS,
|
||||||
expandHqPermissionKeys,
|
expandHqPermissionKeys,
|
||||||
hasAnySystemSettingsPermission,
|
hasAnySystemSettingsPermission,
|
||||||
|
hqBasePermissionKeys,
|
||||||
type HqPermissionKey,
|
type HqPermissionKey,
|
||||||
} from '@dukang/shared-types';
|
} from '@dukang/shared-types';
|
||||||
import { PrismaService } from '../prisma/prisma.module';
|
import { PrismaService } from '../prisma/prisma.module';
|
||||||
@@ -35,30 +35,29 @@ export class HqPermissionsResolver {
|
|||||||
if (!account || account.status !== 'ACTIVE') {
|
if (!account || account.status !== 'ACTIVE') {
|
||||||
throw new ForbiddenException('HQ 账号不可用');
|
throw new ForbiddenException('HQ 账号不可用');
|
||||||
}
|
}
|
||||||
if (account.adminRole === 'SUPER_ADMIN') {
|
|
||||||
return HQ_PERMISSION_CATALOG.map((p) => p.key);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [roleRows, userRows] = await Promise.all([
|
const userRows = await this.prisma.hqAccountPermission.findMany({
|
||||||
this.prisma.hqRolePermission.findMany({
|
|
||||||
where: { adminRole: account.adminRole },
|
|
||||||
select: { permissionKey: true },
|
|
||||||
}),
|
|
||||||
this.prisma.hqAccountPermission.findMany({
|
|
||||||
where: { hqAccountId: actorId },
|
where: { hqAccountId: actorId },
|
||||||
select: { permissionKey: true },
|
select: { permissionKey: true },
|
||||||
}),
|
});
|
||||||
]);
|
const userKeys = userRows.map((r) => r.permissionKey);
|
||||||
|
|
||||||
|
if (account.adminRole === 'SUPER_ADMIN') {
|
||||||
|
// 超管默认拥有业务/管理权限,但不含危险操作;危险权限需按用户显式勾选
|
||||||
|
return expandHqPermissionKeys([...hqBasePermissionKeys(), ...userKeys]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleRows = await this.prisma.hqRolePermission.findMany({
|
||||||
|
where: { adminRole: account.adminRole },
|
||||||
|
select: { permissionKey: true },
|
||||||
|
});
|
||||||
|
|
||||||
const roleKeys =
|
const roleKeys =
|
||||||
roleRows.length > 0
|
roleRows.length > 0
|
||||||
? roleRows.map((r) => r.permissionKey)
|
? roleRows.map((r) => r.permissionKey)
|
||||||
: [...(HQ_ROLE_DEFAULT_PERMISSIONS[account.adminRole] ?? [])];
|
: [...(HQ_ROLE_DEFAULT_PERMISSIONS[account.adminRole] ?? [])];
|
||||||
|
|
||||||
return expandHqPermissionKeys([
|
return expandHqPermissionKeys([...roleKeys, ...userKeys]);
|
||||||
...roleKeys,
|
|
||||||
...userRows.map((r) => r.permissionKey),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||||
import { IsString, MinLength } from 'class-validator';
|
import { IsString, MinLength } from 'class-validator';
|
||||||
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
import { HqAuthGuard } from '../../common/guards/hq-auth.guard';
|
||||||
|
import {
|
||||||
|
HqPermissionGuard,
|
||||||
|
RequireHqPermissions,
|
||||||
|
} from '../../common/guards/hq-permission.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 { AdminCitiesService } from './admin-cities.service';
|
import { AdminCitiesService } from './admin-cities.service';
|
||||||
@@ -24,6 +28,8 @@ export class AdminCitiesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/delete-preview')
|
@Get(':id/delete-preview')
|
||||||
|
@UseGuards(HqPermissionGuard)
|
||||||
|
@RequireHqPermissions('cities_delete')
|
||||||
deletePreview(@Param('id') id: string) {
|
deletePreview(@Param('id') id: string) {
|
||||||
return this.service.deletePreview(BigInt(id));
|
return this.service.deletePreview(BigInt(id));
|
||||||
}
|
}
|
||||||
@@ -51,6 +57,8 @@ export class AdminCitiesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@UseGuards(HqPermissionGuard)
|
||||||
|
@RequireHqPermissions('cities_delete')
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
action: HqOperationAction.CITY_DELETE,
|
action: HqOperationAction.CITY_DELETE,
|
||||||
refType: 'CITY',
|
refType: 'CITY',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
HQ_ROLE_DEFAULT_PERMISSIONS,
|
HQ_ROLE_DEFAULT_PERMISSIONS,
|
||||||
LEGACY_SYSTEM_SETTINGS_KEY,
|
LEGACY_SYSTEM_SETTINGS_KEY,
|
||||||
expandHqPermissionKeys,
|
expandHqPermissionKeys,
|
||||||
|
hqBasePermissionKeys,
|
||||||
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';
|
||||||
@@ -49,7 +50,7 @@ export class AdminHqPermissionsService {
|
|||||||
|
|
||||||
async saveRolePermissions(role: string, permissionKeys: string[]) {
|
async saveRolePermissions(role: string, permissionKeys: string[]) {
|
||||||
if (role === 'SUPER_ADMIN') {
|
if (role === 'SUPER_ADMIN') {
|
||||||
throw new BadRequestException('超级管理员拥有全部权限,无需配置');
|
throw new BadRequestException('超级管理员基础权限固定,危险操作请按用户单独授权');
|
||||||
}
|
}
|
||||||
assertPermissionKeys(permissionKeys);
|
assertPermissionKeys(permissionKeys);
|
||||||
const normalized = expandHqPermissionKeys(permissionKeys);
|
const normalized = expandHqPermissionKeys(permissionKeys);
|
||||||
@@ -73,24 +74,28 @@ export class AdminHqPermissionsService {
|
|||||||
select: { id: true, name: true, phone: true, loginName: true, adminRole: true, status: true },
|
select: { id: true, name: true, phone: true, loginName: true, adminRole: true, status: true },
|
||||||
});
|
});
|
||||||
if (!account) throw new NotFoundException('HQ 账号不存在');
|
if (!account) throw new NotFoundException('HQ 账号不存在');
|
||||||
|
|
||||||
|
const userPerms = await this.prisma.hqAccountPermission.findMany({
|
||||||
|
where: { hqAccountId: accountId },
|
||||||
|
select: { permissionKey: true },
|
||||||
|
});
|
||||||
|
const userPermissionKeys = expandHqPermissionKeys(userPerms.map((p) => p.permissionKey));
|
||||||
|
|
||||||
if (account.adminRole === 'SUPER_ADMIN') {
|
if (account.adminRole === 'SUPER_ADMIN') {
|
||||||
|
const rolePermissionKeys = hqBasePermissionKeys();
|
||||||
|
const effectivePermissionKeys = [
|
||||||
|
...new Set([...rolePermissionKeys, ...userPermissionKeys]),
|
||||||
|
] as HqPermissionKey[];
|
||||||
return serializeBigInt({
|
return serializeBigInt({
|
||||||
account,
|
account,
|
||||||
permissionKeys: HQ_PERMISSION_CATALOG.map((p) => p.key),
|
permissionKeys: userPermissionKeys,
|
||||||
rolePermissionKeys: HQ_PERMISSION_CATALOG.map((p) => p.key),
|
rolePermissionKeys,
|
||||||
userPermissionKeys: [],
|
userPermissionKeys,
|
||||||
effectivePermissionKeys: HQ_PERMISSION_CATALOG.map((p) => p.key),
|
effectivePermissionKeys,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [rolePerms, userPerms] = await Promise.all([
|
const rolePerms = await this.getRolePermissions(account.adminRole);
|
||||||
this.getRolePermissions(account.adminRole),
|
|
||||||
this.prisma.hqAccountPermission.findMany({
|
|
||||||
where: { hqAccountId: accountId },
|
|
||||||
select: { permissionKey: true },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
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[];
|
||||||
@@ -107,9 +112,6 @@ export class AdminHqPermissionsService {
|
|||||||
async saveAccountPermissions(accountId: bigint, permissionKeys: string[]) {
|
async saveAccountPermissions(accountId: bigint, permissionKeys: string[]) {
|
||||||
const account = await this.prisma.hqAccount.findUnique({ where: { id: accountId } });
|
const account = await this.prisma.hqAccount.findUnique({ where: { id: accountId } });
|
||||||
if (!account) throw new NotFoundException('HQ 账号不存在');
|
if (!account) throw new NotFoundException('HQ 账号不存在');
|
||||||
if (account.adminRole === 'SUPER_ADMIN') {
|
|
||||||
throw new BadRequestException('超级管理员拥有全部权限,无需配置');
|
|
||||||
}
|
|
||||||
assertPermissionKeys(permissionKeys);
|
assertPermissionKeys(permissionKeys);
|
||||||
const normalized = expandHqPermissionKeys(permissionKeys);
|
const normalized = expandHqPermissionKeys(permissionKeys);
|
||||||
await this.prisma.$transaction([
|
await this.prisma.$transaction([
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Get, Param, Post, Put, Query, UseGuards } from '@nestjs/common';
|
||||||
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 {
|
||||||
|
HqPermissionGuard,
|
||||||
|
RequireHqPermissions,
|
||||||
|
} from '../../common/guards/hq-permission.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 { AdminOrdersService } from './admin-orders.service';
|
import { AdminOrdersService } from './admin-orders.service';
|
||||||
@@ -18,7 +21,8 @@ export class AdminOrdersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('batch-delete')
|
@Post('batch-delete')
|
||||||
@UseGuards(SuperAdminGuard)
|
@UseGuards(HqPermissionGuard)
|
||||||
|
@RequireHqPermissions('orders_delete')
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
action: HqOperationAction.ORDER_BATCH_DELETE,
|
action: HqOperationAction.ORDER_BATCH_DELETE,
|
||||||
refType: 'ORDER',
|
refType: 'ORDER',
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||||
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 {
|
||||||
|
HqPermissionGuard,
|
||||||
|
RequireHqPermissions,
|
||||||
|
} from '../../common/guards/hq-permission.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 { AdminUsersService } from './admin-users.service';
|
import { AdminUsersService } from './admin-users.service';
|
||||||
@@ -18,13 +21,15 @@ export class AdminUsersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('batch-delete/preview')
|
@Post('batch-delete/preview')
|
||||||
@UseGuards(SuperAdminGuard)
|
@UseGuards(HqPermissionGuard)
|
||||||
|
@RequireHqPermissions('users_delete')
|
||||||
previewBatchDelete(@Body() dto: BatchDeleteUsersDto) {
|
previewBatchDelete(@Body() dto: BatchDeleteUsersDto) {
|
||||||
return this.usersService.previewBatchDelete(dto.ids.map((id) => BigInt(id)));
|
return this.usersService.previewBatchDelete(dto.ids.map((id) => BigInt(id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('batch-delete')
|
@Post('batch-delete')
|
||||||
@UseGuards(SuperAdminGuard)
|
@UseGuards(HqPermissionGuard)
|
||||||
|
@RequireHqPermissions('users_delete')
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
action: HqOperationAction.USER_BATCH_DELETE,
|
action: HqOperationAction.USER_BATCH_DELETE,
|
||||||
refType: 'USER',
|
refType: 'USER',
|
||||||
@@ -44,7 +49,8 @@ export class AdminUsersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@UseGuards(SuperAdminGuard)
|
@UseGuards(HqPermissionGuard)
|
||||||
|
@RequireHqPermissions('users_delete')
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
action: HqOperationAction.USER_DELETE,
|
action: HqOperationAction.USER_DELETE,
|
||||||
refType: 'USER',
|
refType: 'USER',
|
||||||
|
|||||||
Reference in New Issue
Block a user