125 lines
4.5 KiB
Python
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}
|