fix: harden user management operations
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { lazy, Suspense, useState, useCallback, type ReactNode } from 'react';
|
||||
import { lazy, Suspense, useState, useCallback, useEffect, type ReactNode } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { App as AntdApp, ConfigProvider, Spin, theme } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import { getUser } from './store/auth';
|
||||
import { fetchCurrentUser, getUser } from './store/auth';
|
||||
import { ThemeProvider } from './store/theme';
|
||||
import { useTheme } from './store/useTheme';
|
||||
import AntdMessageBridge from './utils/AntdMessageBridge';
|
||||
@@ -44,6 +44,12 @@ function AppContent() {
|
||||
const isLoggedIn = !!getUser();
|
||||
const { isDark } = useTheme();
|
||||
|
||||
// localStorage 仅是前端缓存;每次加载应用时以服务端的实时权限为准。
|
||||
useEffect(() => {
|
||||
if (!getUser()) return;
|
||||
void fetchCurrentUser().finally(refreshAuth);
|
||||
}, [refreshAuth]);
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
locale={zhCN}
|
||||
|
||||
@@ -7,6 +7,8 @@ export const userApi = {
|
||||
api.post<UserInfo, UserInfo>('/users', data),
|
||||
update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string; custom_permissions?: string[] | null }) =>
|
||||
api.put<UserInfo, UserInfo>(`/users/${id}`, data),
|
||||
rename: (id: number, username: string) =>
|
||||
api.patch<UserInfo, UserInfo>(`/users/${id}/username`, { username }),
|
||||
delete: (id: number) => api.delete<MessageResponse, MessageResponse>(`/users/${id}`),
|
||||
listPermissions: () => api.get<PermissionsListResult, PermissionsListResult>('/users/permissions/list'),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import {
|
||||
Table, Button, Modal, Form, Input, Select, Tag, Popconfirm, Space,
|
||||
Checkbox, Divider, Alert, Tooltip,
|
||||
Checkbox, Divider, Alert,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import { message } from '../utils/antdMessage';
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyOutlined } from '@ant-design/icons';
|
||||
import { userApi, type UserInfo } from '../api/modules';
|
||||
@@ -61,6 +62,11 @@ export default function UsersPage() {
|
||||
const { can } = usePermissions();
|
||||
|
||||
const canAssignPerm = can('user:assign_permissions');
|
||||
const canCreate = can('user:create');
|
||||
const canEdit = can('user:edit');
|
||||
const canRename = can('user:rename');
|
||||
const canDelete = can('user:delete');
|
||||
const canManageUser = canEdit || canRename;
|
||||
|
||||
const loadUsers = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -87,6 +93,7 @@ export default function UsersPage() {
|
||||
|
||||
const handleEdit = (user: UserInfo) => {
|
||||
setEditing(user);
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
@@ -99,9 +106,14 @@ export default function UsersPage() {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
if (editing) {
|
||||
const updateData: { role: string; remark: string; password?: string } = { role: values.role, remark: values.remark };
|
||||
if (values.password) updateData.password = values.password;
|
||||
await userApi.update(editing.id, updateData);
|
||||
if (canRename && values.username !== editing.username) {
|
||||
await userApi.rename(editing.id, values.username);
|
||||
}
|
||||
if (canEdit) {
|
||||
const updateData: { role: string; remark: string; password?: string } = { role: values.role, remark: values.remark };
|
||||
if (values.password) updateData.password = values.password;
|
||||
await userApi.update(editing.id, updateData);
|
||||
}
|
||||
message.success('已更新');
|
||||
} else {
|
||||
await userApi.create(values);
|
||||
@@ -170,12 +182,13 @@ export default function UsersPage() {
|
||||
setSelectedPerms(rolePerms);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
const columns: TableProps<UserInfo>['columns'] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 60 },
|
||||
{ title: '用户名', dataIndex: 'username' },
|
||||
{ title: '用户名', dataIndex: 'username', width: 128, ellipsis: true },
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
width: 100,
|
||||
render: (role: string) => {
|
||||
const label = ROLE_OPTIONS.find((r) => r.value === role)?.label || role;
|
||||
return <Tag color={ROLE_COLORS[role] || 'default'}>{label}</Tag>;
|
||||
@@ -196,21 +209,27 @@ export default function UsersPage() {
|
||||
width: 80,
|
||||
render: (active: boolean) => active ? <Tag color="green">启用</Tag> : <Tag>禁用</Tag>,
|
||||
},
|
||||
{ title: '备注', dataIndex: 'remark', ellipsis: true },
|
||||
{ title: '备注', dataIndex: 'remark', width: 180, ellipsis: true },
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
align: 'left',
|
||||
render: (_: unknown, record: UserInfo) => (
|
||||
<Space>
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||||
{canAssignPerm && (
|
||||
<Tooltip title="权限管理">
|
||||
<Button size="small" icon={<SafetyOutlined />} onClick={() => handlePermClick(record)}>权限</Button>
|
||||
</Tooltip>
|
||||
<Space size={4} wrap={false}>
|
||||
{canManageUser && (
|
||||
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||||
)}
|
||||
{record.role !== 'super_admin' && (
|
||||
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
|
||||
<Button danger size="small" icon={<DeleteOutlined />}>删除</Button>
|
||||
{canAssignPerm && (
|
||||
<Button size="small" type="text" icon={<SafetyOutlined />} onClick={() => handlePermClick(record)}>权限</Button>
|
||||
)}
|
||||
{canDelete && record.role !== 'super_admin' && (
|
||||
<Popconfirm
|
||||
title="确认删除该用户?"
|
||||
description="该用户将被停用并解除账号分配,历史任务会保留。"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
>
|
||||
<Button danger size="small" type="text" icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
@@ -222,7 +241,7 @@ export default function UsersPage() {
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<h2>用户管理</h2>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>新建用户</Button>
|
||||
{canCreate && <Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>新建用户</Button>}
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
@@ -230,6 +249,8 @@ export default function UsersPage() {
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
size="small"
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: 844 }}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize,
|
||||
@@ -252,24 +273,30 @@ export default function UsersPage() {
|
||||
open={modalOpen}
|
||||
onCancel={() => setModalOpen(false)}
|
||||
onOk={handleSave}
|
||||
okText="保存"
|
||||
okText={editing && !canEdit ? '保存用户名' : '保存'}
|
||||
width={560}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="username" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input disabled={!!editing} />
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名"
|
||||
extra={editing && canRename ? '改名不会改变用户 ID,账号分配、任务、支付和审计记录均会保留。' : undefined}
|
||||
rules={[{ required: true, message: '请输入用户名' }, { min: 2, max: 64, message: '用户名长度需为 2-64 个字符' }]}
|
||||
>
|
||||
<Input disabled={!!editing && !canRename} autoComplete="off" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={editing ? '新密码(留空不修改)' : '密码'}
|
||||
rules={editing ? [] : [{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password />
|
||||
<Input.Password disabled={!!editing && !canEdit} autoComplete="new-password" placeholder={editing ? '留空不修改' : undefined} />
|
||||
</Form.Item>
|
||||
<Form.Item name="role" label="角色" rules={[{ required: true }]}>
|
||||
<Select options={ROLE_OPTIONS} />
|
||||
<Select disabled={!!editing && !canEdit} options={ROLE_OPTIONS} />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input />
|
||||
<Input disabled={!!editing && !canEdit} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user