224 lines
6.4 KiB
Python
224 lines
6.4 KiB
Python
"""
|
||
认证 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"]
|
||
)
|