79 lines
3.5 KiB
Python
79 lines
3.5 KiB
Python
import secrets
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from pydantic import (
|
|
BaseModel,
|
|
Field as PydanticField,
|
|
)
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from api.depends import get_session, verify_identity_token
|
|
from api.v2.routes.auth._common import _client_ip
|
|
from mail import send_email_verify_code_email, smtp_configured
|
|
from utils import web_email_verify_code as verify_util
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class VerifyEmailRequest(BaseModel):
|
|
code: str = PydanticField(..., min_length=1, max_length=10)
|
|
|
|
|
|
@router.post("/send-verify-code")
|
|
async def send_email_verify_code(
|
|
request: Request,
|
|
session: AsyncSession = Depends(get_session),
|
|
identity=Depends(verify_identity_token),
|
|
):
|
|
"""Отправить код подтверждения email. Требует авторизации."""
|
|
if not smtp_configured():
|
|
raise HTTPException(status_code=503, detail="Почтовый сервер не настроен")
|
|
email = (identity.email or "").strip().lower()
|
|
if not email:
|
|
raise HTTPException(status_code=400, detail="Email не привязан к аккаунту")
|
|
if getattr(identity, "email_verified", False):
|
|
return {"ok": True, "detail": "Email уже подтверждён"}
|
|
if not await verify_util.redis_ready():
|
|
raise HTTPException(status_code=503, detail="Сервис временно недоступен")
|
|
ip = _client_ip(request)
|
|
if not await verify_util.try_consume_ip_send_budget(ip):
|
|
raise HTTPException(status_code=429, detail="Слишком много запросов, попробуйте позже")
|
|
if not await verify_util.try_consume_email_send_budget(email):
|
|
raise HTTPException(status_code=429, detail="Слишком много запросов на этот email")
|
|
if not await verify_util.try_acquire_resend_cooldown(email):
|
|
raise HTTPException(status_code=429, detail="Подождите минуту перед повторной отправкой")
|
|
code = f"{secrets.randbelow(900000) + 100000}"
|
|
await verify_util.store_code(email, code)
|
|
try:
|
|
await send_email_verify_code_email(email, code)
|
|
except Exception:
|
|
await verify_util.delete_code(email)
|
|
raise HTTPException(status_code=503, detail="Не удалось отправить письмо")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/verify-email")
|
|
async def verify_email(
|
|
body: VerifyEmailRequest,
|
|
request: Request,
|
|
session: AsyncSession = Depends(get_session),
|
|
identity=Depends(verify_identity_token),
|
|
):
|
|
"""Подтвердить email по коду."""
|
|
email = (identity.email or "").strip().lower()
|
|
if not email:
|
|
raise HTTPException(status_code=400, detail="Email не привязан к аккаунту")
|
|
if getattr(identity, "email_verified", False):
|
|
return {"ok": True, "detail": "Email уже подтверждён"}
|
|
if not await verify_util.try_consume_verify_budget(email):
|
|
raise HTTPException(status_code=429, detail="Слишком много попыток, попробуйте позже")
|
|
if not await verify_util.verify_and_consume_code(email, body.code.strip()):
|
|
raise HTTPException(status_code=400, detail="Неверный или просроченный код")
|
|
from sqlalchemy import update
|
|
|
|
from database.models import Identity as IdentityModel
|
|
|
|
await session.execute(update(IdentityModel).where(IdentityModel.id == identity.id).values(email_verified=True))
|
|
return {"ok": True}
|