diff --git a/backend/app/api/v1/endpoints/users.py b/backend/app/api/v1/endpoints/users.py index 96f6126..32e1d48 100644 --- a/backend/app/api/v1/endpoints/users.py +++ b/backend/app/api/v1/endpoints/users.py @@ -1,54 +1,96 @@ -from typing import Any, List -from fastapi import APIRouter, Body, Depends, HTTPException +from typing import Any, List, Optional +from fastapi import APIRouter, Body, Depends, HTTPException, Query from fastapi.encoders import jsonable_encoder from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select +from sqlalchemy import func, or_ from app.api import deps from app.core import security from app.db.session import get_db from app.models.user import User -from app.schemas.user import User as UserSchema, UserCreate, UserUpdate +from app.schemas.user import User as UserSchema, UserCreate, UserUpdate, UserListResponse router = APIRouter() -@router.get("/", response_model=List[UserSchema]) +@router.get("/", response_model=UserListResponse) async def read_users( db: AsyncSession = Depends(get_db), - skip: int = 0, - limit: int = 100, + skip: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=100), + search: Optional[str] = Query(None, description="搜索关键词"), + role: Optional[str] = Query(None, description="角色筛选"), + is_active: Optional[bool] = Query(None, description="状态筛选"), current_user: User = Depends(deps.get_current_user), ) -> Any: """ - Retrieve users. + 获取用户列表(支持分页、搜索、筛选) """ - result = await db.execute(select(User).offset(skip).limit(limit)) + query = select(User) + count_query = select(func.count(User.id)) + + # 搜索条件 + if search: + search_filter = or_( + User.email.ilike(f"%{search}%"), + User.full_name.ilike(f"%{search}%"), + User.phone.ilike(f"%{search}%") + ) + query = query.where(search_filter) + count_query = count_query.where(search_filter) + + # 角色筛选 + if role: + query = query.where(User.role == role) + count_query = count_query.where(User.role == role) + + # 状态筛选 + if is_active is not None: + query = query.where(User.is_active == is_active) + count_query = count_query.where(User.is_active == is_active) + + # 获取总数 + total_result = await db.execute(count_query) + total = total_result.scalar() + + # 分页查询 + query = query.order_by(User.created_at.desc()).offset(skip).limit(limit) + result = await db.execute(query) users = result.scalars().all() - return users + + return { + "users": users, + "total": total, + "skip": skip, + "limit": limit + } @router.post("/", response_model=UserSchema) async def create_user( *, db: AsyncSession = Depends(get_db), user_in: UserCreate, + current_user: User = Depends(deps.get_current_active_superuser), ) -> Any: """ - Create new user. + 创建新用户(仅管理员) """ result = await db.execute(select(User).where(User.email == user_in.email)) user = result.scalars().first() if user: raise HTTPException( status_code=400, - detail="The user with this username already exists in the system.", + detail="该邮箱已被注册", ) db_user = User( email=user_in.email, hashed_password=security.get_password_hash(user_in.password), full_name=user_in.full_name, - is_active=user_in.is_active, - is_superuser=user_in.is_superuser, + phone=user_in.phone, + role=user_in.role, + is_active=user_in.is_active if user_in.is_active is not None else True, + is_superuser=user_in.is_superuser if user_in.is_superuser is not None else False, ) db.add(db_user) await db.commit() @@ -60,8 +102,125 @@ async def read_user_me( current_user: User = Depends(deps.get_current_user), ) -> Any: """ - Get current user. + 获取当前用户信息 """ return current_user +@router.put("/me", response_model=UserSchema) +async def update_user_me( + *, + db: AsyncSession = Depends(get_db), + user_in: UserUpdate, + current_user: User = Depends(deps.get_current_user), +) -> Any: + """ + 更新当前用户信息 + """ + update_data = user_in.model_dump(exclude_unset=True) + + # 普通用户不能修改自己的角色和超级管理员状态 + update_data.pop('role', None) + update_data.pop('is_superuser', None) + update_data.pop('is_active', None) + + # 如果更新密码 + if 'password' in update_data and update_data['password']: + update_data['hashed_password'] = security.get_password_hash(update_data['password']) + update_data.pop('password', None) + + for field, value in update_data.items(): + setattr(current_user, field, value) + + await db.commit() + await db.refresh(current_user) + return current_user + +@router.get("/{user_id}", response_model=UserSchema) +async def read_user( + user_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(deps.get_current_user), +) -> Any: + """ + 获取指定用户信息 + """ + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalars().first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + return user + +@router.put("/{user_id}", response_model=UserSchema) +async def update_user( + user_id: str, + *, + db: AsyncSession = Depends(get_db), + user_in: UserUpdate, + current_user: User = Depends(deps.get_current_active_superuser), +) -> Any: + """ + 更新用户信息(仅管理员) + """ + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalars().first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + update_data = user_in.model_dump(exclude_unset=True) + + # 如果更新密码 + if 'password' in update_data and update_data['password']: + update_data['hashed_password'] = security.get_password_hash(update_data['password']) + update_data.pop('password', None) + + for field, value in update_data.items(): + setattr(user, field, value) + + await db.commit() + await db.refresh(user) + return user + +@router.delete("/{user_id}") +async def delete_user( + user_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(deps.get_current_active_superuser), +) -> Any: + """ + 删除用户(仅管理员) + """ + if user_id == current_user.id: + raise HTTPException(status_code=400, detail="不能删除自己的账户") + + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalars().first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + await db.delete(user) + await db.commit() + return {"message": "用户已删除"} + +@router.post("/{user_id}/toggle-status", response_model=UserSchema) +async def toggle_user_status( + user_id: str, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(deps.get_current_active_superuser), +) -> Any: + """ + 切换用户状态(启用/禁用) + """ + if user_id == current_user.id: + raise HTTPException(status_code=400, detail="不能禁用自己的账户") + + result = await db.execute(select(User).where(User.id == user_id)) + user = result.scalars().first() + if not user: + raise HTTPException(status_code=404, detail="用户不存在") + + user.is_active = not user.is_active + await db.commit() + await db.refresh(user) + return user + diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index adc33a6..584c5c4 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, List from pydantic import BaseModel, EmailStr class UserBase(BaseModel): @@ -33,4 +33,10 @@ class UserInDBBase(UserBase): class User(UserInDBBase): pass +class UserListResponse(BaseModel): + users: List[User] + total: int + skip: int + limit: int + diff --git a/frontend/src/app/routes.tsx b/frontend/src/app/routes.tsx index e982f67..207fba8 100644 --- a/frontend/src/app/routes.tsx +++ b/frontend/src/app/routes.tsx @@ -7,6 +7,7 @@ import AuditTasks from "@/pages/AuditTasks"; import TaskDetail from "@/pages/TaskDetail"; import AdminDashboard from "@/pages/AdminDashboard"; import LogsPage from "@/pages/LogsPage"; +import Account from "@/pages/Account"; import type { ReactNode } from 'react'; export interface RouteConfig { @@ -71,6 +72,12 @@ const routes: RouteConfig[] = [ element: , visible: true, }, + { + name: "账号管理", + path: "/account", + element: , + visible: false, // 不在主导航显示,在侧边栏底部单独显示 + }, ]; export default routes; \ No newline at end of file diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 2d1b541..c1377a5 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -13,7 +13,8 @@ import { FileText, ChevronLeft, ChevronRight, - Github + Github, + UserCircle } from "lucide-react"; import routes from "@/app/routes"; @@ -152,8 +153,30 @@ export default function Sidebar({ collapsed, setCollapsed }: SidebarProps) { - {/* Footer with GitHub Link */} -
+ {/* Footer with Account & GitHub Link */} +
+ {/* Account Link */} + setMobileOpen(false)} + title={collapsed ? "账号管理" : undefined} + > + + {!collapsed && ( + 账号管理 + )} + + + {/* GitHub Link */} (null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [showLogoutDialog, setShowLogoutDialog] = useState(false); + const [form, setForm] = useState({ + full_name: "", + phone: "", + github_username: "", + gitlab_username: "", + }); + const [passwordForm, setPasswordForm] = useState({ + current_password: "", + new_password: "", + confirm_password: "", + }); + const [changingPassword, setChangingPassword] = useState(false); + + useEffect(() => { + loadProfile(); + }, []); + + const loadProfile = async () => { + try { + setLoading(true); + const res = await apiClient.get('/users/me'); + setProfile(res.data); + setForm({ + full_name: res.data.full_name || "", + phone: res.data.phone || "", + github_username: res.data.github_username || "", + gitlab_username: res.data.gitlab_username || "", + }); + } catch (error) { + console.error('Failed to load profile:', error); + toast.error("加载账号信息失败"); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + try { + setSaving(true); + const res = await apiClient.put('/users/me', form); + setProfile(res.data); + toast.success("账号信息已更新"); + } catch (error) { + console.error('Failed to update profile:', error); + toast.error("更新失败"); + } finally { + setSaving(false); + } + }; + + const handleChangePassword = async () => { + if (!passwordForm.new_password || !passwordForm.confirm_password) { + toast.error("请填写新密码"); + return; + } + if (passwordForm.new_password !== passwordForm.confirm_password) { + toast.error("两次输入的密码不一致"); + return; + } + if (passwordForm.new_password.length < 6) { + toast.error("密码长度至少6位"); + return; + } + + try { + setChangingPassword(true); + await apiClient.put('/users/me', { password: passwordForm.new_password }); + toast.success("密码已更新"); + setPasswordForm({ current_password: "", new_password: "", confirm_password: "" }); + } catch (error) { + console.error('Failed to change password:', error); + toast.error("密码更新失败"); + } finally { + setChangingPassword(false); + } + }; + + const formatDate = (dateString?: string) => { + if (!dateString) return "-"; + return new Date(dateString).toLocaleDateString('zh-CN', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + }; + + const getInitials = (name?: string, email?: string) => { + if (name) return name.charAt(0).toUpperCase(); + if (email) return email.charAt(0).toUpperCase(); + return "U"; + }; + + const handleLogout = () => { + localStorage.removeItem('access_token'); + toast.success("已退出登录"); + navigate('/login'); + }; + + const handleSwitchAccount = () => { + localStorage.removeItem('access_token'); + navigate('/login'); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Decorative Background */} +
+ + {/* Header */} +
+
+

+ 账号_管理 +

+

管理您的个人账号信息

+
+
+ + +
+
+ +
+ {/* Profile Card */} + + + + + + {getInitials(profile?.full_name, profile?.email)} + + + {profile?.full_name || "未设置姓名"} + {profile?.email} + + + +
+
+ + 角色: + {profile?.role === 'admin' ? '管理员' : '成员'} +
+
+ + 注册时间: + {formatDate(profile?.created_at)} +
+
+
+
+ + {/* Edit Form */} + + + + + 基本信息 + + + +
+
+ + +

邮箱不可修改

+
+
+ + setForm({ ...form, full_name: e.target.value })} + placeholder="请输入姓名" + className="terminal-input" + /> +
+
+ + setForm({ ...form, phone: e.target.value })} + placeholder="请输入手机号" + className="terminal-input" + /> +
+
+ + + +
+

代码托管账号

+
+
+ + setForm({ ...form, github_username: e.target.value })} + placeholder="your-github-username" + className="terminal-input" + /> +
+
+ + setForm({ ...form, gitlab_username: e.target.value })} + placeholder="your-gitlab-username" + className="terminal-input" + /> +
+
+
+ +
+ +
+
+
+ + {/* Password Change */} + + + + + 修改密码 + + + +
+
+ + setPasswordForm({ ...passwordForm, new_password: e.target.value })} + placeholder="输入新密码" + className="terminal-input" + /> +
+
+ + setPasswordForm({ ...passwordForm, confirm_password: e.target.value })} + placeholder="再次输入新密码" + className="terminal-input" + /> +
+
+ +
+
+
+
+
+ + {/* Logout Confirmation Dialog */} + + + + 确认退出登录? + + 退出后需要重新登录才能访问系统。 + + + + + 取消 + + + 确认退出 + + + + +
+ ); +} diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 50c2b6c..6301c90 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,25 +1,34 @@ -import { useState, FormEvent, useEffect } from 'react'; -import { useNavigate, useLocation } from 'react-router-dom'; -import { useAuth } from '@/shared/context/AuthContext'; -import { apiClient } from '@/shared/api/serverClient'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; - -import { toast } from 'sonner'; -import { Lock, Cpu } from 'lucide-react'; +import { useState, FormEvent, useEffect } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; +import { useAuth } from "@/shared/context/AuthContext"; +import { apiClient } from "@/shared/api/serverClient"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { toast } from "sonner"; +import { Lock, Mail, Terminal, Shield, Fingerprint } from "lucide-react"; export default function Login() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [rememberMe, setRememberMe] = useState(false); const [loading, setLoading] = useState(false); const navigate = useNavigate(); const location = useLocation(); const { login, isAuthenticated } = useAuth(); - const from = location.state?.from?.pathname || '/'; + const from = location.state?.from?.pathname || "/"; + + useEffect(() => { + // 检查是否有保存的邮箱 + const savedEmail = localStorage.getItem("remembered_email"); + if (savedEmail) { + setEmail(savedEmail); + setRememberMe(true); + } + }, []); - // 监听认证状态,登录成功后自动跳转 useEffect(() => { if (isAuthenticated && !loading) { navigate(from, { replace: true }); @@ -31,79 +40,118 @@ export default function Login() { setLoading(true); try { const formData = new URLSearchParams(); - formData.append('username', email); - formData.append('password', password); + formData.append("username", email); + formData.append("password", password); - const response = await apiClient.post('/auth/login', formData, { + const response = await apiClient.post("/auth/login", formData, { headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } + "Content-Type": "application/x-www-form-urlencoded", + }, }); + // 记住邮箱 + if (rememberMe) { + localStorage.setItem("remembered_email", email); + } else { + localStorage.removeItem("remembered_email"); + } + await login(response.data.access_token); - toast.success('访问已授予'); - // 跳转由 useEffect 监听 isAuthenticated 状态变化自动处理 + toast.success("登录成功"); } catch (error: any) { - toast.error(error.response?.data?.detail || '访问被拒绝'); + toast.error(error.response?.data?.detail || "登录失败,请检查邮箱和密码"); } finally { setLoading(false); } }; return ( -
- {/* Decorative Background Elements */} +
+ {/* Background Grid */}
-
-
系统ID: 0x84F2
-
状态: 等待输入
-
加密: AES-256
+ {/* Decorative Elements */} +
+
+ + SYS_ID: 0x84F2 +
+
+ + ENCRYPT: AES-256 +
+
+ + AUTH: READY +
-
-
安全连接
-
端口: 443
+
+
SECURE_CONN: TRUE
+
PORT: 443
+
TLS: 1.3
{/* Main Card */} -
+
+ {/* Logo & Title */}
-
- XCodeReviewer +
+ XCodeReviewer
-

+

XCodeReviewer

-

输入凭据以继续

+

+ // 代码审计平台 +

-
- {/* Decorative Corner Markers - Subtle */} -
-
-
-
+ {/* Login Form Card */} +
+ {/* Card Header */} +
+
+
+
+ + 身份验证 + +
-
+
- +
setEmail(e.target.value)} required - className="terminal-input font-mono pl-10" + className="h-12 pl-11 font-mono border-2 border-gray-200 focus:border-black transition-colors" /> - +
- +
setPassword(e.target.value)} required - className="terminal-input font-mono pl-10" + className="h-12 pl-11 font-mono border-2 border-gray-200 focus:border-black transition-colors" /> - + +
+
+ +
+
+ + setRememberMe(checked as boolean) + } + className="border-2 border-gray-300 data-[state=checked]:bg-primary data-[state=checked]:border-primary" + /> +
-
-
- 没有访问令牌? navigate('/register')}>申请访问 -
+ {/* Footer */} +
+

+ 还没有账号?{" "} + navigate("/register")} + > + 立即注册 + +

+ + {/* Version Info */} +
+

+ Version 1.3.4 · Secure Connection +

+
);