This commit is contained in:
Kevin Wong
2026-01-23 18:09:12 +08:00
parent 3a3df41904
commit c918dc6faf
28 changed files with 2250 additions and 126 deletions

223
backend/app/api/auth.py Normal file
View File

@@ -0,0 +1,223 @@
"""
认证 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"]
)