from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel, ConfigDict, Field from typing import Optional, List from datetime import timedelta from sqlalchemy import select from sqlalchemy.orm import selectinload from shared.database.database import get_db_session from shared.services.auth_service import ( authenticate_user, create_access_token, get_current_active_user, get_password_hash ) from shared.models.identity import User, Role, Permission, UserRole, RolePermission from shared.config.settings import settings from shared.utils.logger import get_logger logger = get_logger(__name__) router = APIRouter(prefix="/api/auth", tags=["认证"]) class UserResponse(BaseModel): id: int username: str email: str full_name: Optional[str] is_active: bool is_superuser: bool = False roles: List[str] model_config = ConfigDict(from_attributes=True) class Token(BaseModel): access_token: str token_type: str user: UserResponse class LoginRequest(BaseModel): username: str password: str class RoleCreate(BaseModel): code: str name: str description: Optional[str] = None class RoleResponse(BaseModel): id: int code: str name: str description: Optional[str] is_system: bool permissions: List[str] model_config = ConfigDict(from_attributes=True) class PermissionCreate(BaseModel): code: str name: str module: Optional[str] = None description: Optional[str] = None class PermissionResponse(BaseModel): id: int code: str name: str module: Optional[str] description: Optional[str] model_config = ConfigDict(from_attributes=True) class UserCreate(BaseModel): username: str email: str password: str full_name: Optional[str] = None role_ids: List[int] = [] class UserUpdate(BaseModel): email: Optional[str] = None full_name: Optional[str] = None is_active: Optional[bool] = None role_ids: Optional[List[int]] = None class ResetPasswordRequest(BaseModel): # 新密码走 JSON body(与前端 api-client.ts 的 { new_password } 结构一致)。 # 此前声明为裸 str 参数被 FastAPI 解析为 query param,前端发 body 必然 422, # 重置密码功能端到端断裂;最短 6 位对齐 UsersView 前端校验。 new_password: str = Field(min_length=6) def check_admin(user: User) -> bool: if not user.is_superuser: raise HTTPException(status_code=403, detail="需要管理员权限") return True def _build_user_response(user: User) -> UserResponse: """统一构造用户响应,确保 is_superuser 等字段一致""" return UserResponse( id=user.id, username=user.username, email=user.email, full_name=user.full_name, is_active=user.is_active, is_superuser=user.is_superuser, roles=[r.code for r in user.roles], ) @router.post("/login", response_model=Token) async def login( form_data: OAuth2PasswordRequestForm = Depends(), db_session: AsyncSession = Depends(get_db_session) ): user = await authenticate_user(db_session, form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误", headers={"WWW-Authenticate": "Bearer"}, ) access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) return Token( access_token=access_token, token_type="bearer", user=_build_user_response(user) ) @router.post("/login/json", response_model=Token) async def login_json( login_data: LoginRequest, db_session: AsyncSession = Depends(get_db_session) ): user = await authenticate_user(db_session, login_data.username, login_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误", ) access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) return Token( access_token=access_token, token_type="bearer", user=_build_user_response(user) ) @router.get("/me", response_model=UserResponse) async def get_current_user_info( current_user: User = Depends(get_current_active_user) ): return _build_user_response(current_user) @router.post("/logout") async def logout(): return {"message": "已登出"} @router.get("/users", response_model=List[UserResponse]) async def list_users( db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute( select(User).options(selectinload(User.user_roles).selectinload(UserRole.role)) ) users = result.scalars().all() return [ _build_user_response(u) for u in users ] @router.post("/users", response_model=UserResponse, status_code=201) async def create_user( user_data: UserCreate, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) existing = await db_session.execute( select(User).where(User.username == user_data.username) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="用户名已存在") existing_email = await db_session.execute( select(User).where(User.email == user_data.email) ) if existing_email.scalar_one_or_none(): raise HTTPException(status_code=400, detail="邮箱已存在") try: hashed_password = get_password_hash(user_data.password) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc user = User( username=user_data.username, email=user_data.email, hashed_password=hashed_password, full_name=user_data.full_name, is_active=True ) db_session.add(user) await db_session.flush() for role_id in user_data.role_ids: user_role = UserRole(user_id=user.id, role_id=role_id) db_session.add(user_role) await db_session.commit() await db_session.refresh(user) logger.info(f"管理员 {current_user.username} 创建了用户 {user.username}") return _build_user_response(user) @router.put("/users/{user_id}", response_model=UserResponse) async def update_user( user_id: int, user_data: UserUpdate, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") if user_data.email is not None: user.email = user_data.email if user_data.full_name is not None: user.full_name = user_data.full_name if user_data.is_active is not None: user.is_active = user_data.is_active if user_data.role_ids is not None: await db_session.execute( select(UserRole).where(UserRole.user_id == user_id) ) for ur in (await db_session.execute(select(UserRole).where(UserRole.user_id == user_id))).scalars().all(): await db_session.delete(ur) for role_id in user_data.role_ids: user_role = UserRole(user_id=user.id, role_id=role_id) db_session.add(user_role) await db_session.commit() await db_session.refresh(user) logger.info(f"管理员 {current_user.username} 更新了用户 {user.username}") return _build_user_response(user) @router.delete("/users/{user_id}") async def delete_user( user_id: int, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") if user.id == current_user.id: raise HTTPException(status_code=400, detail="不能删除自己的账户") username = user.username await db_session.delete(user) await db_session.commit() logger.info(f"管理员 {current_user.username} 删除了用户 {username}") return {"message": "用户已删除"} @router.put("/users/{user_id}/reset-password") async def reset_user_password( user_id: int, body: ResetPasswordRequest, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") try: user.hashed_password = get_password_hash(body.new_password) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc await db_session.commit() logger.info(f"管理员 {current_user.username} 重置了用户 {user.username} 的密码") return {"message": "密码已重置"} @router.get("/roles", response_model=List[RoleResponse]) async def list_roles( db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute(select(Role)) roles = result.scalars().all() return [ RoleResponse( id=r.id, code=r.code, name=r.name, description=r.description, is_system=r.is_system, permissions=[p.code for p in r.permissions] ) for r in roles ] @router.post("/roles", response_model=RoleResponse, status_code=201) async def create_role( role_data: RoleCreate, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) existing = await db_session.execute( select(Role).where(Role.code == role_data.code) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="角色编码已存在") role = Role( code=role_data.code, name=role_data.name, description=role_data.description ) db_session.add(role) await db_session.commit() await db_session.refresh(role) logger.info(f"管理员 {current_user.username} 创建了角色 {role.code}") return RoleResponse( id=role.id, code=role.code, name=role.name, description=role.description, is_system=role.is_system, permissions=[] ) @router.put("/roles/{role_id}", response_model=RoleResponse) async def update_role( role_id: int, role_data: RoleCreate, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute(select(Role).where(Role.id == role_id)) role = result.scalar_one_or_none() if not role: raise HTTPException(status_code=404, detail="角色不存在") if role.is_system: raise HTTPException(status_code=400, detail="系统角色不能修改") role.name = role_data.name role.description = role_data.description await db_session.commit() await db_session.refresh(role) return RoleResponse( id=role.id, code=role.code, name=role.name, description=role.description, is_system=role.is_system, permissions=[p.code for p in role.permissions] ) @router.delete("/roles/{role_id}") async def delete_role( role_id: int, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute(select(Role).where(Role.id == role_id)) role = result.scalar_one_or_none() if not role: raise HTTPException(status_code=404, detail="角色不存在") if role.is_system: raise HTTPException(status_code=400, detail="系统角色不能删除") await db_session.delete(role) await db_session.commit() logger.info(f"管理员 {current_user.username} 删除了角色 {role.code}") return {"message": "角色已删除"} @router.put("/roles/{role_id}/permissions") async def set_role_permissions( role_id: int, permission_ids: List[int], db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute(select(Role).where(Role.id == role_id)) role = result.scalar_one_or_none() if not role: raise HTTPException(status_code=404, detail="角色不存在") for rp in (await db_session.execute(select(RolePermission).where(RolePermission.role_id == role_id))).scalars().all(): await db_session.delete(rp) for perm_id in permission_ids: rp = RolePermission(role_id=role_id, permission_id=perm_id) db_session.add(rp) await db_session.commit() logger.info(f"管理员 {current_user.username} 更新了角色 {role.code} 的权限") return {"message": "权限已更新"} @router.get("/permissions", response_model=List[PermissionResponse]) async def list_permissions( db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute(select(Permission)) permissions = result.scalars().all() return [PermissionResponse.from_orm(p) for p in permissions] @router.post("/permissions", response_model=PermissionResponse, status_code=201) async def create_permission( perm_data: PermissionCreate, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) existing = await db_session.execute( select(Permission).where(Permission.code == perm_data.code) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="权限编码已存在") permission = Permission( code=perm_data.code, name=perm_data.name, module=perm_data.module, description=perm_data.description ) db_session.add(permission) await db_session.commit() await db_session.refresh(permission) logger.info(f"管理员 {current_user.username} 创建了权限 {permission.code}") return PermissionResponse.from_orm(permission) @router.delete("/permissions/{permission_id}") async def delete_permission( permission_id: int, db_session: AsyncSession = Depends(get_db_session), current_user: User = Depends(get_current_active_user) ): check_admin(current_user) result = await db_session.execute(select(Permission).where(Permission.id == permission_id)) permission = result.scalar_one_or_none() if not permission: raise HTTPException(status_code=404, detail="权限不存在") await db_session.delete(permission) await db_session.commit() logger.info(f"管理员 {current_user.username} 删除了权限 {permission.code}") return {"message": "权限已删除"}