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}