diff --git a/README.md b/README.md index 87b194e..8fc55f6 100644 --- a/README.md +++ b/README.md @@ -341,7 +341,7 @@ MuMuAINovel/ ## 📧 联系方式 - 提交 [Issue](https://github.com/xiamuceer-j/MuMuAINovel/issues) -- Linux DO [讨论](https://linux.do/t/topic/1100112) +- Linux DO [讨论](https://linux.do/t/topic/1106333) --- diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py new file mode 100644 index 0000000..2edd5f0 --- /dev/null +++ b/backend/app/api/admin.py @@ -0,0 +1,393 @@ +""" +管理员API - 用户管理功能 +""" +from fastapi import APIRouter, HTTPException, Request, Depends +from pydantic import BaseModel, Field +from typing import Optional, List +from datetime import datetime +import hashlib +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from app.database import get_db, init_db +from app.models.user import User +from app.user_manager import user_manager +from app.user_password import password_manager +from app.logger import get_logger + +logger = get_logger(__name__) + +router = APIRouter(prefix="/admin", tags=["管理员"]) + + +# ==================== 请求/响应模型 ==================== + +class CreateUserRequest(BaseModel): + """创建用户请求""" + username: str = Field(..., min_length=3, max_length=20, description="用户名") + display_name: str = Field(..., min_length=2, max_length=50, description="显示名称") + password: Optional[str] = Field(None, min_length=6, description="初始密码,留空则自动生成") + avatar_url: Optional[str] = Field(None, description="头像URL") + trust_level: int = Field(0, ge=0, le=9, description="信任等级") + is_admin: bool = Field(False, description="是否为管理员") + + +class UpdateUserRequest(BaseModel): + """更新用户请求""" + display_name: Optional[str] = Field(None, min_length=2, max_length=50) + avatar_url: Optional[str] = None + trust_level: Optional[int] = Field(None, ge=-1, le=9) + is_admin: Optional[bool] = Field(None, description="是否为管理员") + + +class ToggleStatusRequest(BaseModel): + """切换用户状态请求""" + is_active: bool = Field(..., description="true=启用, false=禁用") + + +class ResetPasswordRequest(BaseModel): + """重置密码请求""" + new_password: Optional[str] = Field(None, min_length=6, description="新密码,留空则重置为默认密码") + + +class UserResponse(BaseModel): + """用户响应""" + user_id: str + username: str + display_name: str + avatar_url: Optional[str] + trust_level: int + is_admin: bool + is_active: bool + linuxdo_id: str + created_at: str + last_login: Optional[str] + + +class CreateUserResponse(BaseModel): + """创建用户响应""" + success: bool + message: str + user: dict + default_password: Optional[str] = None + + +# ==================== 权限检查依赖 ==================== + +async def check_admin(request: Request) -> User: + """检查管理员权限""" + user = getattr(request.state, "user", None) + if not user: + raise HTTPException(status_code=401, detail="未登录") + + if not user.is_admin: + raise HTTPException(status_code=403, detail="需要管理员权限") + + return user + + +# ==================== API 端点 ==================== + +@router.get("/users") +async def get_users( + admin: User = Depends(check_admin), + db: AsyncSession = Depends(get_db) +): + """获取用户列表(仅管理员)""" + try: + all_users = await user_manager.get_all_users() + + users_data = [] + for user in all_users: + # user_manager 返回的是 Pydantic User 对象,直接转为 dict + user_dict = user.model_dump() + user_dict["is_active"] = user.trust_level != -1 + users_data.append(user_dict) + + logger.info(f"管理员 {admin.user_id} 获取用户列表,共 {len(users_data)} 个用户") + + return { + "total": len(users_data), + "users": users_data + } + except Exception as e: + logger.error(f"获取用户列表失败: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"获取用户列表失败: {str(e)}") + + +@router.post("/users") +async def create_user( + data: CreateUserRequest, + admin: User = Depends(check_admin), + db: AsyncSession = Depends(get_db) +): + """添加用户(仅管理员)""" + try: + # 检查用户名是否已存在 + all_users = await user_manager.get_all_users() + for user in all_users: + if user.username == data.username: + raise HTTPException(status_code=409, detail="用户名已存在") + + # 生成用户ID + user_id = f"admin_created_{hashlib.md5(data.username.encode()).hexdigest()[:16]}" + + # 创建用户 + new_user = await user_manager.create_or_update_from_linuxdo( + linuxdo_id=user_id, + username=data.username, + display_name=data.display_name, + avatar_url=data.avatar_url, + trust_level=data.trust_level + ) + + # 设置管理员标志 + if data.is_admin: + # 直接更新数据库中的is_admin字段 + async with await user_manager._get_session() as session: + result = await session.execute( + select(User).where(User.user_id == user_id) + ) + db_user = result.scalar_one_or_none() + if db_user: + db_user.is_admin = True + await session.commit() + new_user.is_admin = True + + # 设置密码 + actual_password = await password_manager.set_password( + user_id=new_user.user_id, + username=data.username, + password=data.password + ) + + # 初始化用户数据库 + try: + await init_db(new_user.user_id) + logger.info(f"用户 {new_user.user_id} 数据库初始化成功") + except Exception as e: + logger.error(f"用户 {new_user.user_id} 数据库初始化失败: {e}") + + logger.info(f"管理员 {admin.user_id} 创建了新用户 {new_user.user_id} ({data.username})") + + return CreateUserResponse( + success=True, + message="用户创建成功", + user=new_user.model_dump(), + default_password=actual_password if not data.password else None + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"创建用户失败: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"创建用户失败: {str(e)}") + + +@router.put("/users/{user_id}") +async def update_user( + user_id: str, + data: UpdateUserRequest, + admin: User = Depends(check_admin), + db: AsyncSession = Depends(get_db) +): + """编辑用户信息(仅管理员)""" + try: + # 获取目标用户 + target_user = await user_manager.get_user(user_id) + if not target_user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 更新用户信息 + async with await user_manager._get_session() as session: + result = await session.execute( + select(User).where(User.user_id == user_id) + ) + db_user = result.scalar_one_or_none() + + if not db_user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 更新字段 + if data.display_name is not None: + db_user.display_name = data.display_name + if data.avatar_url is not None: + db_user.avatar_url = data.avatar_url + if data.trust_level is not None: + db_user.trust_level = data.trust_level + if data.is_admin is not None: + # 检查是否是最后一个管理员 + if db_user.is_admin and not data.is_admin: + all_users = await user_manager.get_all_users() + admin_count = sum(1 for u in all_users if u.is_admin) + if admin_count <= 1: + raise HTTPException(status_code=400, detail="不能取消最后一个管理员的权限") + db_user.is_admin = data.is_admin + + await session.commit() + await session.refresh(db_user) + + logger.info(f"管理员 {admin.user_id} 更新了用户 {user_id} 的信息") + + updated_user = await user_manager.get_user(user_id) + user_dict = updated_user.model_dump() + user_dict["is_active"] = updated_user.trust_level != -1 + + return { + "success": True, + "message": "用户信息更新成功", + "user": user_dict + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"更新用户失败: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"更新用户失败: {str(e)}") + + +@router.post("/users/{user_id}/toggle-status") +async def toggle_user_status( + user_id: str, + data: ToggleStatusRequest, + admin: User = Depends(check_admin), + db: AsyncSession = Depends(get_db) +): + """切换用户状态(启用/禁用)(仅管理员)""" + try: + # 不允许禁用自己 + if user_id == admin.user_id: + raise HTTPException(status_code=400, detail="不能禁用自己的账号") + + # 获取目标用户 + target_user = await user_manager.get_user(user_id) + if not target_user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 更新状态 + async with await user_manager._get_session() as session: + result = await session.execute( + select(User).where(User.user_id == user_id) + ) + db_user = result.scalar_one_or_none() + + if not db_user: + raise HTTPException(status_code=404, detail="用户不存在") + + if data.is_active: + # 启用用户:恢复trust_level为0(或之前的值) + db_user.trust_level = 0 + else: + # 禁用用户:设置trust_level为-1 + db_user.trust_level = -1 + + await session.commit() + + status_text = "启用" if data.is_active else "禁用" + logger.info(f"管理员 {admin.user_id} {status_text}了用户 {user_id}") + + return { + "success": True, + "message": f"用户已{status_text}", + "is_active": data.is_active + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"切换用户状态失败: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"切换用户状态失败: {str(e)}") + + +@router.post("/users/{user_id}/reset-password") +async def reset_password( + user_id: str, + data: ResetPasswordRequest, + admin: User = Depends(check_admin), + db: AsyncSession = Depends(get_db) +): + """重置用户密码(仅管理员)""" + try: + # 获取目标用户 + target_user = await user_manager.get_user(user_id) + if not target_user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 重置密码 + actual_password = await password_manager.set_password( + user_id=user_id, + username=target_user.username, + password=data.new_password + ) + + logger.info(f"管理员 {admin.user_id} 重置了用户 {user_id} 的密码") + + return { + "success": True, + "message": "密码重置成功", + "new_password": actual_password + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"重置密码失败: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"重置密码失败: {str(e)}") + + +@router.delete("/users/{user_id}") +async def delete_user( + user_id: str, + admin: User = Depends(check_admin), + db: AsyncSession = Depends(get_db) +): + """删除用户(仅管理员,慎用)""" + try: + # 不允许删除自己 + if user_id == admin.user_id: + raise HTTPException(status_code=400, detail="不能删除自己的账号") + + # 获取目标用户 + target_user = await user_manager.get_user(user_id) + if not target_user: + raise HTTPException(status_code=404, detail="用户不存在") + + # 检查是否是最后一个管理员 + if target_user.is_admin: + all_users = await user_manager.get_all_users() + admin_count = sum(1 for u in all_users if u.is_admin) + if admin_count <= 1: + raise HTTPException(status_code=400, detail="不能删除最后一个管理员账号") + + # 删除用户(包括密码记录) + async with await user_manager._get_session() as session: + # 删除用户记录 + result = await session.execute( + select(User).where(User.user_id == user_id) + ) + db_user = result.scalar_one_or_none() + if db_user: + await session.delete(db_user) + + # 删除密码记录 + from app.models.user import UserPassword + result = await session.execute( + select(UserPassword).where(UserPassword.user_id == user_id) + ) + pwd_record = result.scalar_one_or_none() + if pwd_record: + await session.delete(pwd_record) + + await session.commit() + + logger.warning(f"管理员 {admin.user_id} 删除了用户 {user_id}") + + return { + "success": True, + "message": "用户已删除" + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"删除用户失败: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=f"删除用户失败: {str(e)}") \ No newline at end of file diff --git a/backend/app/main.py b/backend/app/main.py index 8c97c05..dcaefa0 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -135,12 +135,13 @@ from app.api import ( projects, outlines, characters, chapters, wizard_stream, relationships, organizations, auth, users, settings, writing_styles, memories, - mcp_plugins + mcp_plugins, admin ) app.include_router(auth.router, prefix="/api") app.include_router(users.router, prefix="/api") app.include_router(settings.router, prefix="/api") +app.include_router(admin.router, prefix="/api") app.include_router(projects.router, prefix="/api") app.include_router(wizard_stream.router, prefix="/api") diff --git a/backend/app/middleware/auth_middleware.py b/backend/app/middleware/auth_middleware.py index e173504..9a1de87 100644 --- a/backend/app/middleware/auth_middleware.py +++ b/backend/app/middleware/auth_middleware.py @@ -1,9 +1,12 @@ """ 认证中间件 - 从 Cookie 中提取用户信息并注入到 request.state """ -from fastapi import Request +from fastapi import Request, HTTPException from starlette.middleware.base import BaseHTTPMiddleware from app.user_manager import user_manager +from app.logger import get_logger + +logger = get_logger(__name__) class AuthMiddleware(BaseHTTPMiddleware): @@ -20,9 +23,18 @@ class AuthMiddleware(BaseHTTPMiddleware): if user_id: user = await user_manager.get_user(user_id) if user: - request.state.user_id = user_id - request.state.user = user - request.state.is_admin = user.is_admin + # 检查用户是否被禁用 (trust_level = -1) + if user.trust_level == -1: + logger.warning(f"禁用用户尝试访问: {user_id} ({user.username})") + # 清除用户状态,视为未登录 + request.state.user_id = None + request.state.user = None + request.state.is_admin = False + else: + # 用户正常,注入状态 + request.state.user_id = user_id + request.state.user = user + request.state.is_admin = user.is_admin else: # 用户不存在,清除状态 request.state.user_id = None diff --git a/backend/app/services/mcp_test_service.py b/backend/app/services/mcp_test_service.py index a656172..e0fcd38 100644 --- a/backend/app/services/mcp_test_service.py +++ b/backend/app/services/mcp_test_service.py @@ -164,6 +164,9 @@ class MCPTestService: # 转换为OpenAI Function Calling格式 openai_tools = self._convert_tools_to_openai_format(tools) + logger.info(f"📋 转换后的OpenAI工具数量: {len(openai_tools)}") + logger.debug(f"📋 OpenAI工具列表: {[t['function']['name'] for t in openai_tools]}") + # 调用AI选择工具 prompt = f"""你是MCP插件测试助手,需要测试插件 '{plugin.plugin_name}' 的功能。 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0ca4de0..1df1b24 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,6 +15,7 @@ import ChapterAnalysis from './pages/ChapterAnalysis'; import WritingStyles from './pages/WritingStyles'; import Settings from './pages/Settings'; import MCPPlugins from './pages/MCPPlugins'; +import UserManagement from './pages/UserManagement'; // import Polish from './pages/Polish'; import Login from './pages/Login'; import AuthCallback from './pages/AuthCallback'; @@ -38,6 +39,7 @@ function App() { } /> } /> } /> + } /> } /> }> } /> diff --git a/frontend/src/components/UserMenu.tsx b/frontend/src/components/UserMenu.tsx index 05891ec..92262c4 100644 --- a/frontend/src/components/UserMenu.tsx +++ b/frontend/src/components/UserMenu.tsx @@ -1,20 +1,17 @@ import { useState, useEffect } from 'react'; -import { Dropdown, Avatar, Space, Typography, message, Modal, Table, Button, Tag, Popconfirm, Pagination, Form, Input } from 'antd'; -import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined, KeyOutlined } from '@ant-design/icons'; -import { authApi, userApi } from '../services/api'; +import { Dropdown, Avatar, Space, Typography, message, Modal, Form, Input, Button } from 'antd'; +import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons'; +import { authApi } from '../services/api'; import type { User } from '../types'; import type { MenuProps } from 'antd'; +import { useNavigate } from 'react-router-dom'; const { Text } = Typography; export default function UserMenu() { + const navigate = useNavigate(); const [currentUser, setCurrentUser] = useState(null); - const [showUserManagement, setShowUserManagement] = useState(false); const [showChangePassword, setShowChangePassword] = useState(false); - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(false); - const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(10); const [changePasswordForm] = Form.useForm(); const [changingPassword, setChangingPassword] = useState(false); @@ -42,98 +39,12 @@ export default function UserMenu() { } }; - const handleShowUserManagement = async () => { + const handleShowUserManagement = () => { if (!currentUser?.is_admin) { message.warning('只有管理员可以访问用户管理'); return; } - - setShowUserManagement(true); - loadUsers(); - }; - - const loadUsers = async () => { - try { - setLoading(true); - const userList = await userApi.listUsers(); - setUsers(userList); - } catch (error) { - console.error('获取用户列表失败:', error); - message.error('获取用户列表失败'); - } finally { - setLoading(false); - } - }; - - const handleSetAdmin = async (userId: string, isAdmin: boolean) => { - try { - await userApi.setAdmin(userId, isAdmin); - message.success(isAdmin ? '已设置为管理员' : '已取消管理员权限'); - loadUsers(); - } catch (error) { - console.error('设置管理员失败:', error); - message.error('设置管理员失败'); - } - }; - - const handleDeleteUser = async (userId: string) => { - try { - await userApi.deleteUser(userId); - message.success('用户已删除'); - loadUsers(); - } catch (error) { - console.error('删除用户失败:', error); - message.error('删除用户失败'); - } - }; - - const handleResetPassword = async (userId: string, username: string) => { - Modal.confirm({ - title: '重置用户密码', - content: ( -
-

确定要重置用户 {username} 的密码吗?

-

密码将被重置为默认密码:{username}@666

-
- ), - okText: '确定重置', - cancelText: '取消', - onOk: async () => { - try { - const result = await userApi.resetPassword(userId); - if (result.default_password) { - Modal.success({ - title: '密码重置成功', - content: ( -
-

用户 {result.username} 的密码已重置为:

-

- {result.default_password} -

-

- 请将此密码告知用户,建议用户登录后立即修改密码 -

-
- ), - }); - } else { - message.success('密码重置成功'); - } - loadUsers(); - } catch (error: any) { - console.error('重置密码失败:', error); - message.error(error.response?.data?.detail || '重置密码失败'); - } - }, - }); + navigate('/user-management'); }; const handleChangePassword = async (values: { oldPassword: string; newPassword: string }) => { @@ -169,14 +80,17 @@ export default function UserMenu() { { type: 'divider', }, - ...(currentUser?.is_admin ? [{ - key: 'user-management', - icon: , - label: '用户管理', - onClick: handleShowUserManagement, - }, { - type: 'divider' as const, - }] : []), + ...(currentUser?.is_admin ? [ + { + key: 'user-management', + icon: , + label: '用户管理', + onClick: handleShowUserManagement, + }, + { + type: 'divider' as const, + } + ] : []), { key: 'change-password', icon: , @@ -194,95 +108,6 @@ export default function UserMenu() { }, ]; - const columns = [ - { - title: '用户名', - dataIndex: 'username', - key: 'username', - render: (text: string, record: User) => ( - - } size="small" /> -
-
{record.display_name || text}
- {text} -
-
- ), - }, - { - title: 'Trust Level', - dataIndex: 'trust_level', - key: 'trust_level', - width: 120, - render: (level: number) => {level}, - }, - { - title: '角色', - dataIndex: 'is_admin', - key: 'is_admin', - width: 100, - render: (isAdmin: boolean) => ( - isAdmin ? }>管理员 : 普通用户 - ), - }, - { - title: '最后登录', - dataIndex: 'last_login', - key: 'last_login', - width: 180, - render: (date: string) => new Date(date).toLocaleString('zh-CN'), - }, - { - title: '操作', - key: 'actions', - width: 280, - render: (_: unknown, record: User) => { - const isSelf = record.user_id === currentUser?.user_id; - return ( - - {record.is_admin ? ( - handleSetAdmin(record.user_id, false)} - disabled={isSelf} - > - - - ) : ( - - )} - - handleDeleteUser(record.user_id)} - disabled={isSelf} - > - - - - ); - }, - }, - ]; - if (!currentUser) { return null; } @@ -365,68 +190,6 @@ export default function UserMenu() { - setShowUserManagement(false)} - footer={null} - width={900} - centered - styles={{ - body: { - padding: 0, - display: 'flex', - flexDirection: 'column', - height: 'calc(100vh - 380px)', - } - }} - > -
-
- - -
- `共 ${total} 个用户`} - pageSizeOptions={['10', '20', '50', '100']} - onChange={(page, newPageSize) => { - setCurrentPage(page); - setPageSize(newPageSize); - }} - /> -
- - - handleToggle(plugin, checked)} size={isMobile ? 'small' : 'default'} + style={{ + flexShrink: 0, + height: isMobile ? 16 : 22, + minHeight: isMobile ? 16 : 22, + lineHeight: isMobile ? '16px' : '22px' + }} /> diff --git a/frontend/src/pages/UserManagement.tsx b/frontend/src/pages/UserManagement.tsx new file mode 100644 index 0000000..35cb48b --- /dev/null +++ b/frontend/src/pages/UserManagement.tsx @@ -0,0 +1,767 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + Table, + Button, + Modal, + Form, + Input, + Switch, + Space, + Tag, + Popconfirm, + message, + Card, + Typography, + Badge, + InputNumber, + Tooltip, + Row, + Col, + Pagination, + Dropdown, +} from 'antd'; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + KeyOutlined, + StopOutlined, + CheckCircleOutlined, + ArrowLeftOutlined, + TeamOutlined, + UserOutlined, + SearchOutlined, + MoreOutlined, +} from '@ant-design/icons'; +import { adminApi } from '../services/api'; +import type { User } from '../types'; +import UserMenu from '../components/UserMenu'; + +const { Title, Text } = Typography; + +interface UserWithStatus extends User { + is_active?: boolean; +} + +export default function UserManagement() { + const navigate = useNavigate(); + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [editModalVisible, setEditModalVisible] = useState(false); + const [resetPasswordModalVisible, setResetPasswordModalVisible] = useState(false); + const [currentUser, setCurrentUser] = useState(null); + const [newPassword, setNewPassword] = useState(''); + const [pageSize, setPageSize] = useState(20); + const [currentPage, setCurrentPage] = useState(1); + const [searchText, setSearchText] = useState(''); + + const [form] = Form.useForm(); + const [editForm] = Form.useForm(); + + // 过滤用户列表 + const filteredUsers = users.filter(user => { + if (!searchText) return true; + const searchLower = searchText.toLowerCase(); + return ( + user.username?.toLowerCase().includes(searchLower) || + user.display_name?.toLowerCase().includes(searchLower) || + user.user_id?.toLowerCase().includes(searchLower) + ); + }); + + // 加载用户列表 + const loadUsers = async () => { + setLoading(true); + try { + const res = await adminApi.getUsers(); + setUsers(res.users); + } catch (error) { + console.error('加载用户列表失败:', error); + message.error('加载用户列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadUsers(); + }, []); + + // 添加用户 + const handleCreate = async (values: any) => { + try { + const res = await adminApi.createUser(values); + message.success('用户创建成功'); + + // 如果有默认密码,显示给管理员 + if (res.default_password) { + Modal.info({ + title: '用户创建成功', + content: ( +
+

用户名:{values.username}

+

初始密码:{res.default_password}

+

+ ⚠️ 请复制密码并告知用户,此密码仅显示一次! +

+
+ ), + width: 500, + }); + } + + setModalVisible(false); + form.resetFields(); + loadUsers(); + } catch (error) { + console.error('创建用户失败:', error); + message.error('创建用户失败'); + } + }; + + // 编辑用户 + const handleEdit = (user: UserWithStatus) => { + setCurrentUser(user); + editForm.setFieldsValue({ + display_name: user.display_name, + avatar_url: user.avatar_url, + trust_level: user.trust_level, + is_admin: user.is_admin, + }); + setEditModalVisible(true); + }; + + const handleUpdate = async (values: any) => { + if (!currentUser) return; + + try { + await adminApi.updateUser(currentUser.user_id, values); + message.success('用户信息更新成功'); + setEditModalVisible(false); + editForm.resetFields(); + loadUsers(); + } catch (error) { + console.error('更新用户失败:', error); + message.error('更新用户失败'); + } + }; + + // 切换用户状态 + const handleToggleStatus = async (user: UserWithStatus) => { + const isActive = user.is_active !== false; + const action = isActive ? '禁用' : '启用'; + + try { + await adminApi.toggleUserStatus(user.user_id, !isActive); + message.success(`用户已${action}`); + loadUsers(); + } catch (error) { + console.error(`${action}用户失败:`, error); + message.error(`${action}用户失败`); + } + }; + + // 重置密码 + const handleResetPassword = (user: UserWithStatus) => { + setCurrentUser(user); + setNewPassword(''); + setResetPasswordModalVisible(true); + }; + + const handleResetPasswordConfirm = async () => { + if (!currentUser) return; + + try { + const res = await adminApi.resetPassword( + currentUser.user_id, + newPassword || undefined + ); + + Modal.info({ + title: '密码重置成功', + content: ( +
+

用户:{currentUser.username}

+

新密码:{res.new_password}

+

+ ⚠️ 请复制密码并告知用户! +

+
+ ), + width: 500, + }); + + setResetPasswordModalVisible(false); + setNewPassword(''); + } catch (error) { + console.error('重置密码失败:', error); + message.error('重置密码失败'); + } + }; + + // 删除用户 + const handleDelete = async (user: UserWithStatus) => { + try { + await adminApi.deleteUser(user.user_id); + message.success('用户已删除'); + loadUsers(); + } catch (error) { + console.error('删除用户失败:', error); + message.error('删除用户失败'); + } + }; + + const isMobile = window.innerWidth <= 768; + + // 表格列定义 + const columns = [ + { + title: '用户名', + dataIndex: 'username', + key: 'username', + width: 150, + render: (text: string) => ( + + + {text} + + ), + }, + { + title: '显示名称', + dataIndex: 'display_name', + key: 'display_name', + width: 150, + }, + { + title: '状态', + dataIndex: 'is_active', + key: 'is_active', + width: 100, + render: (isActive: boolean) => ( + + ), + }, + { + title: '角色', + dataIndex: 'is_admin', + key: 'is_admin', + width: 100, + render: (isAdmin: boolean) => ( + + {isAdmin ? '👑 管理员' : '普通用户'} + + ), + }, + { + title: '信任等级', + dataIndex: 'trust_level', + key: 'trust_level', + width: 100, + render: (level: number) => ( + = 5 ? 'green' : 'blue'}> + {level === -1 ? '已禁用' : `Level ${level}`} + + ), + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + width: 180, + render: (date: string) => date ? new Date(date).toLocaleString('zh-CN') : '-', + }, + { + title: '最后登录', + dataIndex: 'last_login', + key: 'last_login', + width: 180, + render: (date: string) => date ? new Date(date).toLocaleString('zh-CN') : '从未登录', + }, + { + title: '操作', + key: 'action', + width: isMobile ? 80 : 300, + fixed: 'right' as const, + render: (_: any, record: UserWithStatus) => { + const isActive = record.is_active !== false; + + // 移动端:使用下拉菜单 + if (isMobile) { + const menuItems = [ + { + key: 'edit', + label: '编辑用户', + icon: , + onClick: () => handleEdit(record), + }, + { + key: 'reset', + label: '重置密码', + icon: , + onClick: () => handleResetPassword(record), + }, + { + key: 'toggle', + label: isActive ? '禁用用户' : '启用用户', + icon: isActive ? : , + danger: isActive, + onClick: () => { + Modal.confirm({ + title: `确定${isActive ? '禁用' : '启用'}该用户吗?`, + onOk: () => handleToggleStatus(record), + okText: '确定', + cancelText: '取消', + }); + }, + }, + ...(!record.is_admin ? [{ + key: 'delete', + label: '删除用户', + icon: , + danger: true, + onClick: () => { + Modal.confirm({ + title: '确定删除该用户吗?此操作不可恢复!', + onOk: () => handleDelete(record), + okText: '确定', + cancelText: '取消', + okButtonProps: { danger: true }, + }); + }, + }] : []), + ]; + + return ( + + +
+ + + + + + handleToggleStatus(record)} + okText="确定" + cancelText="取消" + > + + + + + + {!record.is_admin && ( + handleDelete(record)} + okText="确定" + cancelText="取消" + okButtonProps={{ danger: true }} + > + + + + + )} + + ); + }, + }, + ]; + + return ( +
+
+ {/* 顶部导航卡片 */} + + +
+ + + <TeamOutlined style={{ color: '#fa8c16', marginRight: 8 }} /> + 用户管理 + + + 管理系统用户和权限 + + + + + + + + + + + + + + {/* 主内容卡片 */} + + {/* 搜索栏 */} +
+ } + value={searchText} + onChange={(e) => { + setSearchText(e.target.value); + setCurrentPage(1); // 搜索时重置到第一页 + }} + allowClear + style={{ + borderRadius: 8, + }} + /> +
+ + {/* 表格区域 */} +
+
+ + + {/* 固定分页控件 */} +
+ `共 ${total} 个用户${searchText ? ' (已过滤)' : ''}`} + pageSizeOptions={[20, 50, 100]} + onChange={(page, size) => { + setCurrentPage(page); + setPageSize(size); + }} + onShowSizeChange={(_current, size) => { + setCurrentPage(1); + setPageSize(size); + }} + /> +
+ + + + {/* 添加用户对话框 */} + 添加用户} + open={modalVisible} + onCancel={() => { + setModalVisible(false); + form.resetFields(); + }} + onOk={() => form.submit()} + width={isMobile ? '90%' : 600} + centered + okText="创建" + cancelText="取消" + > +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ + {/* 编辑用户对话框 */} + 编辑用户} + open={editModalVisible} + onCancel={() => { + setEditModalVisible(false); + editForm.resetFields(); + }} + onOk={() => editForm.submit()} + width={isMobile ? '90%' : 600} + centered + okText="保存" + cancelText="取消" + > +
+ + + + + + + + + + + + + + + + +
+ + {/* 重置密码对话框 */} + 重置密码} + open={resetPasswordModalVisible} + onCancel={() => { + setResetPasswordModalVisible(false); + setNewPassword(''); + }} + onOk={handleResetPasswordConfirm} + width={isMobile ? '90%' : 500} + centered + okText="确认重置" + cancelText="取消" + > +
+ 用户:{currentUser?.username} +
+
+ + setNewPassword(e.target.value)} + placeholder="留空则使用默认密码" + /> + + +
+ + ); +} \ No newline at end of file diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index ffeba23..68fe452 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -520,4 +520,62 @@ export const mcpPluginApi = { // 调用工具 callTool: (data: MCPToolCallRequest) => api.post('/mcp/call', data), +}; + +// 管理员API +export const adminApi = { + // 获取用户列表 + getUsers: () => + api.get('/admin/users'), + + // 添加用户 + createUser: (data: { + username: string; + display_name: string; + password?: string; + avatar_url?: string; + trust_level?: number; + is_admin?: boolean; + }) => + api.post('/admin/users', data), + + // 编辑用户 + updateUser: (userId: string, data: { + display_name?: string; + avatar_url?: string; + trust_level?: number; + }) => + api.put(`/admin/users/${userId}`, data), + + // 切换用户状态(启用/禁用) + toggleUserStatus: (userId: string, isActive: boolean) => + api.post(`/admin/users/${userId}/toggle-status`, { is_active: isActive }), + + // 重置密码 + resetPassword: (userId: string, newPassword?: string) => + api.post(`/admin/users/${userId}/reset-password`, { new_password: newPassword }), + + // 删除用户 + deleteUser: (userId: string) => + api.delete(`/admin/users/${userId}`), }; \ No newline at end of file