Files
gsfdc/server/app/api/site/auth.py
T

125 lines
4.5 KiB
Python

from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.core.security import create_access_token
from app.db import get_db
from app.models.member import create_member, get_member_by_email, set_member_password
from app.services.email_code import (
consume_email_code,
is_valid_email,
issue_email_code,
normalize_email,
)
from app.services.password import hash_password, validate_password, verify_password
router = APIRouter()
class EmailCodeRequest(BaseModel):
email: str
purpose: str
class RegisterRequest(BaseModel):
email: str
code: str
password: str
privacyAccepted: bool = False
class LoginRequest(BaseModel):
email: str
password: str
code: str | None = None
class ResetPasswordRequest(BaseModel):
email: str
code: str
password: str
def _bad(detail: str) -> None:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
@router.post("/auth/email-code")
def send_email_code(body: EmailCodeRequest, db: Session = Depends(get_db)) -> dict:
purpose = (body.purpose or "").strip()
email = normalize_email(body.email)
if purpose not in {"register", "login", "reset"}:
_bad("验证码用途不正确")
if not is_valid_email(email):
_bad("邮箱格式不正确")
existing = get_member_by_email(db, email)
if purpose == "register" and existing is not None:
_bad("该邮箱已注册")
if purpose in {"login", "reset"} and existing is None:
_bad("该邮箱尚未注册")
if purpose in {"login", "reset"} and existing is not None and existing.status != "active":
_bad("账号不可用")
try:
issue_email_code(email=email, purpose=purpose)
except ValueError as exc:
_bad(str(exc))
return {"ok": True}
@router.post("/auth/register")
def register(body: RegisterRequest, db: Session = Depends(get_db)) -> dict:
email = normalize_email(body.email)
if not is_valid_email(email):
_bad("邮箱格式不正确")
if not body.privacyAccepted:
_bad("请先阅读并同意隐私政策")
if get_member_by_email(db, email) is not None:
_bad("该邮箱已注册")
# 先校验密码,避免弱密码拒收时已消费验证码,用户改密后无法再用同一码。
validate_password(body.password)
if not consume_email_code(email=email, purpose="register", code=body.code):
_bad("验证码不正确或已过期")
member = create_member(
db,
email=email,
password_hash=hash_password(body.password),
privacy_accepted_at=datetime.now(timezone.utc).replace(tzinfo=None),
)
token = create_access_token(str(member.id), "member")
return {"accessToken": token, "tokenType": "member", "email": member.email}
@router.post("/auth/login")
def login(body: LoginRequest, db: Session = Depends(get_db)) -> dict:
email = normalize_email(body.email)
if not is_valid_email(email):
_bad("邮箱格式不正确")
member = get_member_by_email(db, email)
if member is None or member.status != "active":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="邮箱或密码不正确")
if body.code is not None and str(body.code).strip():
if not consume_email_code(email=email, purpose="login", code=body.code):
_bad("验证码不正确或已过期")
if not verify_password(body.password, member.password_hash):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="邮箱或密码不正确")
token = create_access_token(str(member.id), "member")
return {"accessToken": token, "tokenType": "member", "email": member.email}
@router.post("/auth/reset-password")
def reset_password(body: ResetPasswordRequest, db: Session = Depends(get_db)) -> dict:
email = normalize_email(body.email)
if not is_valid_email(email):
_bad("邮箱格式不正确")
member = get_member_by_email(db, email)
if member is None or member.status != "active":
_bad("该邮箱尚未注册")
validate_password(body.password)
if not consume_email_code(email=email, purpose="reset", code=body.code):
_bad("验证码不正确或已过期")
set_member_password(db, member, hash_password(body.password))
token = create_access_token(str(member.id), "member")
return {"accessToken": token, "tokenType": "member", "email": member.email}