feat: добавлены задачи для отправки email и обновлены сервисы аутентификации
- Созданы две задачи с использованием Celery: `send_text_confirmation_email` и `send_confirmation_email`. - Обновлена модель бизнес-логики `UserService`: добавлен метод `confirm_user` для подтверждения пользователя. - Обновлена модель менеджера `UserManager`: добавлен асинхронный метод `confirm_user` для обновления состояния пользователя в базе данных. - Добавлена новая роутер-функция для подтверждения регистрации по ссылке `/register_confirm`.
This commit is contained in:
parent
84446d44ce
commit
217af1cd06
@ -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()
|
||||
|
@ -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": "Электронная почта подтверждена"}
|
||||
|
@ -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
75
lkeep/apps/auth/tasks.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user