feat(admin): add user-level direct permission system and enhance activity tracking
Features: - Add user_permissions table for direct user-to-permission grants (ops:user-ops) - Merge role_permissions + user_permissions in auth chain (login, middleware, getCurrentUser) - Add getUserQueryScope support for USER role with ops:user-ops (cross-tenant access) - Unify cross-tenant operation checks via getUserQueryScope (remove hardcoded SUPER_ADMIN checks) - Add 3 new API endpoints: GET/PUT /:id/permissions, GET /options/permissions - Support ops:user-ops as alternative permission on all user/tenant management routes - Frontend: add user-ops permission toggle on UserFormPage and UserDetailPage - Enhance DC module activity tracking (StreamAIController, SessionController, QuickActionController) - Fix DC AIController user ID extraction and feature name consistency - Add verify-activity-tracking.ts validation script - Update deployment checklist and admin module documentation DB Migration: 20260309_add_user_permissions_table Made-with: Cursor
This commit is contained in:
@@ -104,6 +104,29 @@ export async function importUsers(
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户直授权限
|
||||
*/
|
||||
export async function getUserDirectPermissions(userId: string): Promise<string[]> {
|
||||
const response = await apiClient.get<{ code: number; data: { permissions: string[] } }>(`${BASE_URL}/${userId}/permissions`);
|
||||
return response.data.data.permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户直授权限
|
||||
*/
|
||||
export async function updateUserDirectPermissions(userId: string, permissions: string[]): Promise<void> {
|
||||
await apiClient.put(`${BASE_URL}/${userId}/permissions`, { permissions });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可直授权限列表
|
||||
*/
|
||||
export async function getPermissionOptions(): Promise<Array<{ code: string; name: string; description: string | null }>> {
|
||||
const response = await apiClient.get<{ code: number; data: Array<{ code: string; name: string; description: string | null }> }>(`${BASE_URL}/options/permissions`);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有租户列表(用于下拉选择)
|
||||
*/
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Modal,
|
||||
Tooltip,
|
||||
Empty,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
@@ -50,14 +51,20 @@ const UserDetailPage: React.FC = () => {
|
||||
const [assignTenantVisible, setAssignTenantVisible] = useState(false);
|
||||
const [modulePermissionVisible, setModulePermissionVisible] = useState(false);
|
||||
const [selectedMembership, setSelectedMembership] = useState<TenantMembership | null>(null);
|
||||
const [directPermissions, setDirectPermissions] = useState<string[]>([]);
|
||||
const [opsToggling, setOpsToggling] = useState(false);
|
||||
|
||||
// 加载用户详情
|
||||
const loadUser = async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await userApi.getUserById(id);
|
||||
const [data, perms] = await Promise.all([
|
||||
userApi.getUserById(id),
|
||||
userApi.getUserDirectPermissions(id),
|
||||
]);
|
||||
setUser(data);
|
||||
setDirectPermissions(perms);
|
||||
} catch (error) {
|
||||
console.error('加载用户详情失败:', error);
|
||||
message.error('加载用户详情失败');
|
||||
@@ -142,6 +149,24 @@ const UserDetailPage: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 切换用户运营权限
|
||||
const handleToggleUserOps = async (checked: boolean) => {
|
||||
if (!user) return;
|
||||
setOpsToggling(true);
|
||||
try {
|
||||
const newPerms = checked
|
||||
? [...directPermissions.filter(p => p !== 'ops:user-ops'), 'ops:user-ops']
|
||||
: directPermissions.filter(p => p !== 'ops:user-ops');
|
||||
await userApi.updateUserDirectPermissions(user.id, newPerms);
|
||||
setDirectPermissions(newPerms);
|
||||
message.success(checked ? '已开启用户运营权限' : '已关闭用户运营权限');
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
setOpsToggling(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 打开模块权限配置
|
||||
const handleConfigModules = (membership: TenantMembership) => {
|
||||
setSelectedMembership(membership);
|
||||
@@ -323,7 +348,17 @@ const UserDetailPage: React.FC = () => {
|
||||
<Descriptions.Item label="创建时间">
|
||||
{new Date(user.createdAt).toLocaleString('zh-CN')}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后登录" span={3}>
|
||||
<Descriptions.Item label="用户运营权限">
|
||||
<Switch
|
||||
checked={directPermissions.includes('ops:user-ops')}
|
||||
onChange={handleToggleUserOps}
|
||||
loading={opsToggling}
|
||||
checkedChildren="已开启"
|
||||
unCheckedChildren="未开启"
|
||||
size="small"
|
||||
/>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最后登录" span={2}>
|
||||
{user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString('zh-CN') : '从未登录'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Divider,
|
||||
Checkbox,
|
||||
Alert,
|
||||
Switch,
|
||||
} from 'antd';
|
||||
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@@ -48,6 +49,7 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
|
||||
const [departmentOptions, setDepartmentOptions] = useState<DepartmentOption[]>([]);
|
||||
const [moduleOptions, setModuleOptions] = useState<ModuleOption[]>([]);
|
||||
const [selectedTenantId, setSelectedTenantId] = useState<string>();
|
||||
const [hasUserOps, setHasUserOps] = useState(false);
|
||||
|
||||
// 加载租户选项
|
||||
useEffect(() => {
|
||||
@@ -74,8 +76,11 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
|
||||
useEffect(() => {
|
||||
if (mode === 'edit' && id) {
|
||||
setLoading(true);
|
||||
userApi.getUserById(id)
|
||||
.then((user) => {
|
||||
Promise.all([
|
||||
userApi.getUserById(id),
|
||||
userApi.getUserDirectPermissions(id),
|
||||
])
|
||||
.then(([user, directPerms]) => {
|
||||
form.setFieldsValue({
|
||||
phone: user.phone,
|
||||
name: user.name,
|
||||
@@ -85,6 +90,7 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
|
||||
departmentId: user.department?.id,
|
||||
});
|
||||
setSelectedTenantId(user.defaultTenant.id);
|
||||
setHasUserOps(directPerms.includes('ops:user-ops'));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('加载用户数据失败:', error);
|
||||
@@ -115,7 +121,11 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
|
||||
tenantRole: values.tenantRole || values.role,
|
||||
allowedModules: values.allowedModules?.length > 0 ? values.allowedModules : undefined,
|
||||
};
|
||||
await userApi.createUser(data);
|
||||
const created = await userApi.createUser(data);
|
||||
// 创建后追加直授权限
|
||||
if (hasUserOps) {
|
||||
await userApi.updateUserDirectPermissions(created.id, ['ops:user-ops']);
|
||||
}
|
||||
message.success('用户创建成功');
|
||||
} else {
|
||||
const data: UpdateUserRequest = {
|
||||
@@ -125,6 +135,8 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
|
||||
departmentId: values.departmentId || undefined,
|
||||
};
|
||||
await userApi.updateUser(id!, data);
|
||||
// 更新直授权限
|
||||
await userApi.updateUserDirectPermissions(id!, hasUserOps ? ['ops:user-ops'] : []);
|
||||
message.success('用户更新成功');
|
||||
}
|
||||
navigate('/admin/users');
|
||||
@@ -222,6 +234,23 @@ const UserFormPage: React.FC<UserFormPageProps> = ({ mode }) => {
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 用户运营权限 */}
|
||||
<Row gutter={24}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="用户运营权限"
|
||||
tooltip="开启后该用户可访问运营管理端的租户管理、用户管理、运营日志模块"
|
||||
>
|
||||
<Switch
|
||||
checked={hasUserOps}
|
||||
onChange={setHasUserOps}
|
||||
checkedChildren="已开启"
|
||||
unCheckedChildren="未开启"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{mode === 'create' && (
|
||||
<Alert
|
||||
message="默认密码"
|
||||
|
||||
Reference in New Issue
Block a user