完善邮件验证与密码重置安全流程
This commit is contained in:
+107
-2
@@ -1,7 +1,7 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from fastapi import File, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
@@ -12,7 +12,19 @@ from app.core.security import create_access_token, decode_token_allow_expired, o
|
||||
from app.core.deps import get_current_user, get_db_session
|
||||
from app.crud import user as user_crud
|
||||
from app.models.user import UserStatus
|
||||
from app.schemas.email_settings import (
|
||||
EmailCodeResponse,
|
||||
EmailCodeVerifyResponse,
|
||||
PasswordResetCodeVerifyRequest,
|
||||
PasswordResetCodeVerifyResponse,
|
||||
PasswordResetLinkSendRequest,
|
||||
PasswordResetRequest,
|
||||
PasswordResetTokenRequest,
|
||||
RegisterEmailCodeSendRequest,
|
||||
RegisterEmailCodeVerifyRequest,
|
||||
)
|
||||
from app.schemas.user import Token, UserRead, UserRegisterRequest, UserSelfUpdate, UserUpdate
|
||||
from app.services import email_service
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
|
||||
@@ -39,6 +51,14 @@ class ExtendResponse(BaseModel):
|
||||
expiresAt: datetime
|
||||
|
||||
|
||||
class EmailAvailabilityResponse(BaseModel):
|
||||
available: bool
|
||||
|
||||
|
||||
class EmailDomainsResponse(BaseModel):
|
||||
items: list[str]
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
AVATAR_ROOT = Path(__file__).resolve().parent.parent.parent / "uploads" / "avatars"
|
||||
AVATAR_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
@@ -108,6 +128,90 @@ def ensure_user_active(db_user) -> None:
|
||||
)
|
||||
|
||||
|
||||
@router.get("/email-domains", response_model=EmailDomainsResponse)
|
||||
async def read_email_domains(
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailDomainsResponse:
|
||||
rows = await email_service.list_email_settings(db)
|
||||
return EmailDomainsResponse(items=[row.register_domain for row in rows])
|
||||
|
||||
|
||||
@router.get("/register/email-availability", response_model=EmailAvailabilityResponse)
|
||||
async def check_register_email_availability(
|
||||
email: EmailStr = Query(...),
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailAvailabilityResponse:
|
||||
existing = await user_crud.get_by_email(db, str(email))
|
||||
return EmailAvailabilityResponse(available=existing is None)
|
||||
|
||||
|
||||
@router.post("/register/email-code/send", response_model=EmailCodeResponse)
|
||||
async def send_register_email_code(
|
||||
payload: RegisterEmailCodeSendRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.send_register_code(db, str(payload.email))
|
||||
return EmailCodeResponse(message="验证码已发送")
|
||||
|
||||
|
||||
@router.post("/register/email-code/verify", response_model=EmailCodeVerifyResponse)
|
||||
async def verify_register_email_code(
|
||||
payload: RegisterEmailCodeVerifyRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeVerifyResponse:
|
||||
await email_service.verify_register_code(db, str(payload.email), payload.code)
|
||||
return EmailCodeVerifyResponse(verified=True)
|
||||
|
||||
|
||||
@router.post("/password-reset/email-code/send", response_model=EmailCodeResponse)
|
||||
async def send_password_reset_email_code(
|
||||
payload: PasswordResetLinkSendRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.send_password_reset_code(db, str(payload.email))
|
||||
return EmailCodeResponse(message="验证码发送成功,请查收邮箱")
|
||||
|
||||
|
||||
@router.post("/password-reset/email-code/verify", response_model=PasswordResetCodeVerifyResponse)
|
||||
async def verify_password_reset_email_code(
|
||||
payload: PasswordResetCodeVerifyRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> PasswordResetCodeVerifyResponse:
|
||||
reset_token = await email_service.verify_password_reset_code(db, str(payload.email), payload.code)
|
||||
return PasswordResetCodeVerifyResponse(verified=True, reset_token=reset_token)
|
||||
|
||||
|
||||
@router.post("/password-reset-link/send", response_model=EmailCodeResponse)
|
||||
async def send_password_reset_link(
|
||||
payload: PasswordResetLinkSendRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.send_password_reset_link(
|
||||
db,
|
||||
str(payload.email),
|
||||
frontend_origin=settings.FRONTEND_PUBLIC_URL,
|
||||
)
|
||||
return EmailCodeResponse(message="如果账号存在,重置链接已发送")
|
||||
|
||||
|
||||
@router.post("/password-reset", response_model=EmailCodeResponse)
|
||||
async def reset_password(
|
||||
payload: PasswordResetRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.reset_password_with_code(db, str(payload.email), payload.code, payload.password)
|
||||
return EmailCodeResponse(message="密码已重置,请返回登录")
|
||||
|
||||
|
||||
@router.post("/password-reset-link", response_model=EmailCodeResponse)
|
||||
async def reset_password_with_link(
|
||||
payload: PasswordResetTokenRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> EmailCodeResponse:
|
||||
await email_service.reset_password_with_token(db, payload.token, payload.password)
|
||||
return EmailCodeResponse(message="密码已重置,请返回登录")
|
||||
|
||||
|
||||
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
||||
async def register(
|
||||
payload: UserRegisterRequest,
|
||||
@@ -116,6 +220,7 @@ async def register(
|
||||
existing = await user_crud.get_by_email(db, payload.email)
|
||||
if existing:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="邮箱已注册")
|
||||
await email_service.ensure_register_email_verified(db, str(payload.email))
|
||||
await user_crud.create_registered_user(db, payload)
|
||||
return {"message": "注册成功,请登录"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user