feat: добавлены задачи для отправки email и обновлены сервисы аутентификации

- Созданы две задачи с использованием Celery: `send_text_confirmation_email` и `send_confirmation_email`.
- Обновлена модель бизнес-логики `UserService`: добавлен метод `confirm_user` для подтверждения пользователя.
- Обновлена модель менеджера `UserManager`: добавлен асинхронный метод `confirm_user` для обновления состояния пользователя в базе данных.
- Добавлена новая роутер-функция для подтверждения регистрации по ссылке `/register_confirm`.
This commit is contained in:
proDream 2025-03-20 13:43:22 +04:00
parent 84446d44ce
commit 217af1cd06
4 changed files with 133 additions and 9 deletions

View File

@ -7,7 +7,7 @@ https://pressanybutton.ru/category/servis-na-fastapi/
"""
from fastapi import Depends, HTTPException
from sqlalchemy import insert
from sqlalchemy import insert, update
from sqlalchemy.exc import IntegrityError
from lkeep.apps.auth.schemas import CreateUser, UserReturnData
@ -24,8 +24,6 @@ class UserManager:
"""
Инициализирует экземпляр класса.
:param model: Модель, используемая для работы с данными.
:type model: Type[User]
:param db: Зависимость от базы данных. По умолчанию используется Depends(DBDependency).
:type db: DBDependency
"""
@ -42,7 +40,7 @@ class UserManager:
:rtype: UserReturnData
:raises HTTPException: Если пользователь уже существует.
"""
async with self.db.db_session as session:
async with self.db.db_session() as session:
query = insert(self.model).values(**user.model_dump()).returning(self.model)
try:
@ -52,5 +50,17 @@ class UserManager:
await session.commit()
user_data = await result.scalar_one()
user_data = result.scalar_one()
return UserReturnData(**user_data.__dict__)
async def confirm_user(self, email: str) -> None:
"""
Асинхронный метод для подтверждения пользователя по электронной почте.
:param email: Электронная почта пользователя, которого нужно подтвердить.
:type email: str
"""
async with self.db.db_session() as session:
query = update(self.model).where(self.model.email == email).values(is_verified=True, is_active=True)
await session.execute(query)
await session.commit()

View File

@ -29,3 +29,18 @@ async def registration(user: RegisterUser, service: UserService = Depends(UserSe
:raises HTTPException 400: Если данные пользователя некорректны.
"""
return await service.register_user(user=user)
@auth_router.get(path="/register_confirm", status_code=status.HTTP_200_OK)
async def confirm_registration(token: str, service: UserService = Depends(UserService)) -> dict[str, str]:
"""
Подтверждает регистрацию пользователя по ссылке.
:param token: Токен подтверждения регистрации, полученный после отправки на электронную почту.
:type token: str
:raises HTTPException: Если токен недействителен или срок действия истек.
:return: Словарь с сообщением о успешной подтверждении электронной почты.
:rtype: dict[str, str]
"""
await service.confirm_user(token=token)
return {"message": "Электронная почта подтверждена"}

View File

@ -6,11 +6,14 @@
https://pressanybutton.ru/category/servis-na-fastapi/
"""
from fastapi import Depends
from fastapi import Depends, HTTPException
from itsdangerous import BadSignature, URLSafeTimedSerializer
from lkeep.apps.auth.handlers import AuthHandler
from lkeep.apps.auth.managers import UserManager
from lkeep.apps.auth.schemas import CreateUser, RegisterUser, UserReturnData
from lkeep.apps.auth.tasks import send_confirmation_email
from lkeep.core.settings import settings
class UserService:
@ -31,18 +34,39 @@ class UserService:
"""
self.manager = manager
self.handler = handler
self.serializer = URLSafeTimedSerializer(secret_key=settings.secret_key.get_secret_value())
async def register_user(self, user: RegisterUser) -> UserReturnData:
"""
Регистрирует нового пользователя в системе.
:param user: Данные для регистрации пользователя.
:param user: Информация о пользователе, который нужно зарегистрировать.
:type user: RegisterUser
:return: Данные зарегистрированного пользователя.
:returns: Данные о созданном пользователе.
:rtype: UserReturnData
"""
hashed_password = await self.handler.get_password_hash(user.password)
new_user = CreateUser(email=user.email, hashed_password=hashed_password)
return await self.manager.create_user(user=new_user)
user_data = await self.manager.create_user(user=new_user)
confirmation_token = self.serializer.dumps(user_data.email)
send_confirmation_email.delay(to_email=user_data.email, token=confirmation_token)
return user_data
async def confirm_user(self, token: str) -> None:
"""
Подтверждает пользователя по переданному токену.
:param token: Токен для подтверждения пользователя.
:type token: str
:raises HTTPException: Если токен неверный или просроченный.
"""
try:
email = self.serializer.loads(token, max_age=3600)
except BadSignature:
raise HTTPException(status_code=400, detail="Неверный или просроченный токен")
await self.manager.confirm_user(email=email)

75
lkeep/apps/auth/tasks.py Normal file
View File

@ -0,0 +1,75 @@
"""
Проект: Lkeep
Автор: Иван Ашихмин
Год: 2025
Специально для проекта "Код на салфетке"
https://pressanybutton.ru/category/servis-na-fastapi/
"""
import smtplib
from email.message import EmailMessage
from celery import shared_task
from starlette.templating import Jinja2Templates
from lkeep.core.settings import settings
@shared_task
def send_text_confirmation_email(to_email: str, token: str) -> None:
"""
Отправляет текстовое подтверждение регистрации по электронной почте.
:param to_email: Адрес электронной почты получателя подтверждения.
:type to_email: str
:param token: Токен для подтверждения регистрации.
:type token: str
"""
confirmation_url = f"{settings.frontend_url}/auth/register_confirm?token={token}"
text = f"""Спасибо за регистрацию!
Для подтверждения регистрации перейдите по ссылке: {confirmation_url}
"""
message = EmailMessage()
message.set_content(text)
message["From"] = settings.email_settings.email_username
message["To"] = to_email
message["Subject"] = "Подтверждение регистрации"
with smtplib.SMTP_SSL(host=settings.email_settings.email_host, port=settings.email_settings.email_port) as smtp:
smtp.login(
user=settings.email_settings.email_username,
password=settings.email_settings.email_password.get_secret_value(),
)
smtp.send_message(msg=message)
@shared_task
def send_confirmation_email(to_email: str, token: str) -> None:
"""
Отправляет подтверждение регистрации по электронной почте.
:param to_email: Адрес электронной почты получателя сообщения.
:type to_email: str
:param token: Токен для подтверждения регистрации, передаваемый в URL.
:type token: str
"""
confirmation_url = f"{settings.frontend_url}/auth/register_confirm?token={token}"
templates = Jinja2Templates(directory=settings.templates_dir)
template = templates.get_template(name="confirmation_email.html")
html_content = template.render(confirmation_url=confirmation_url)
message = EmailMessage()
message.add_alternative(html_content, subtype="html")
message["From"] = settings.email_settings.email_username
message["To"] = to_email
message["Subject"] = "Подтверждение регистрации"
with smtplib.SMTP_SSL(host=settings.email_settings.email_host, port=settings.email_settings.email_port) as smtp:
smtp.login(
user=settings.email_settings.email_username,
password=settings.email_settings.email_password.get_secret_value(),
)
smtp.send_message(msg=message)