""" 认证 API:注册、登录、登出 """ from fastapi import APIRouter, HTTPException, Response, status, Request from pydantic import BaseModel, EmailStr from app.core.supabase import get_supabase from app.core.security import ( get_password_hash, verify_password, create_access_token, generate_session_token, set_auth_cookie, clear_auth_cookie, decode_access_token ) from loguru import logger from typing import Optional router = APIRouter(prefix="/api/auth", tags=["认证"]) class RegisterRequest(BaseModel): email: EmailStr password: str username: Optional[str] = None class LoginRequest(BaseModel): email: EmailStr password: str class UserResponse(BaseModel): id: str email: str username: Optional[str] role: str is_active: bool @router.post("/register") async def register(request: RegisterRequest): """ 用户注册 注册后状态为 pending,需要管理员激活 """ try: supabase = get_supabase() # 检查邮箱是否已存在 existing = supabase.table("users").select("id").eq( "email", request.email ).execute() if existing.data: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="该邮箱已注册" ) # 创建用户 password_hash = get_password_hash(request.password) result = supabase.table("users").insert({ "email": request.email, "password_hash": password_hash, "username": request.username or request.email.split("@")[0], "role": "pending", "is_active": False }).execute() logger.info(f"新用户注册: {request.email}") return { "success": True, "message": "注册成功,请等待管理员审核激活" } except HTTPException: raise except Exception as e: logger.error(f"注册失败: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="注册失败,请稍后重试" ) @router.post("/login") async def login(request: LoginRequest, response: Response): """ 用户登录 - 验证密码 - 检查是否激活 - 实现"后踢前"单设备登录 """ try: supabase = get_supabase() # 查找用户 user_result = supabase.table("users").select("*").eq( "email", request.email ).single().execute() user = user_result.data if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="邮箱或密码错误" ) # 验证密码 if not verify_password(request.password, user["password_hash"]): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="邮箱或密码错误" ) # 检查是否激活 if not user["is_active"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="账号未激活,请等待管理员审核" ) # 检查授权是否过期 if user.get("expires_at"): from datetime import datetime, timezone expires_at = datetime.fromisoformat(user["expires_at"].replace("Z", "+00:00")) if datetime.now(timezone.utc) > expires_at: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="授权已过期,请联系管理员续期" ) # 生成新的 session_token (后踢前) session_token = generate_session_token() # 删除旧 session,插入新 session supabase.table("user_sessions").delete().eq( "user_id", user["id"] ).execute() supabase.table("user_sessions").insert({ "user_id": user["id"], "session_token": session_token, "device_info": None # 可以从 request headers 获取 }).execute() # 生成 JWT Token token = create_access_token(user["id"], session_token) # 设置 HttpOnly Cookie set_auth_cookie(response, token) logger.info(f"用户登录: {request.email}") return { "success": True, "message": "登录成功", "user": UserResponse( id=user["id"], email=user["email"], username=user.get("username"), role=user["role"], is_active=user["is_active"] ) } except HTTPException: raise except Exception as e: logger.error(f"登录失败: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="登录失败,请稍后重试" ) @router.post("/logout") async def logout(response: Response): """用户登出""" clear_auth_cookie(response) return {"success": True, "message": "已登出"} @router.get("/me") async def get_me(request: Request): """获取当前用户信息""" # 从 Cookie 获取用户 token = request.cookies.get("access_token") if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="未登录" ) token_data = decode_access_token(token) if not token_data: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token 无效" ) supabase = get_supabase() user_result = supabase.table("users").select("*").eq( "id", token_data.user_id ).single().execute() user = user_result.data if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在" ) return UserResponse( id=user["id"], email=user["email"], username=user.get("username"), role=user["role"], is_active=user["is_active"] )