diff --git a/app/chat/router.py b/app/chat/router.py index 58c42cf..00dee05 100644 --- a/app/chat/router.py +++ b/app/chat/router.py @@ -4,7 +4,17 @@ from fastapi import APIRouter, Depends, status from app.config import settings from app.exceptions import UserDontHavePermissionException, MessageNotFoundException, UserCanNotReadThisChatException -from app.chat.shemas import SMessage, SLastMessages, SPinnedChat, SDeletedUser, SChat, SDeletedChat +from app.chat.shemas import ( + SMessage, + SLastMessages, + SPinnedChat, + SDeletedUser, + SDeletedChat, + SAllowedChats, + SMessageList, + SPinnedChats, + SPinnedMessages +) from app.unit_of_work import UnitOfWork from app.users.dependencies import check_verificated_user_with_exc @@ -17,7 +27,7 @@ router = APIRouter(prefix="/chat", tags=["Чат"]) @router.get( "", status_code=status.HTTP_200_OK, - response_model=list[SChat] + response_model=SAllowedChats, ) async def get_all_chats(user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): async with uow: @@ -30,7 +40,12 @@ async def get_all_chats(user: SUser = Depends(check_verificated_user_with_exc), status_code=status.HTTP_201_CREATED, response_model=None, ) -async def create_chat(user_to_exclude: int, chat_name: str, user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): +async def create_chat( + user_to_exclude: int, + chat_name: str, + user: SUser = Depends(check_verificated_user_with_exc), + uow=Depends(UnitOfWork) +): async with uow: if user.id == user_to_exclude: raise UserCanNotReadThisChatException @@ -43,7 +58,7 @@ async def create_chat(user_to_exclude: int, chat_name: str, user: SUser = Depend @router.get( "/get_last_message/{chat_id}", status_code=status.HTTP_200_OK, - response_model=list[SMessage] + response_model=SMessageList, ) async def get_last_message(chat_id: int, user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): await AuthService.validate_user_access_to_chat(uow=uow, chat_id=chat_id, user_id=user.id) @@ -59,10 +74,13 @@ async def get_last_message(chat_id: int, user: SUser = Depends(check_verificated @router.get( "/get_some_messages/{chat_id}", status_code=status.HTTP_200_OK, - response_model=list[SMessage] + response_model=SMessageList, ) async def get_some_messages( - chat_id: int, last_messages: SLastMessages = Depends(), user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork) + chat_id: int, + last_messages: SLastMessages = Depends(), + user: SUser = Depends(check_verificated_user_with_exc), + uow=Depends(UnitOfWork) ): await AuthService.validate_user_access_to_chat(uow=uow, chat_id=chat_id, user_id=user.id) async with uow: @@ -81,9 +99,14 @@ async def get_some_messages( @router.get( "/message/{chat_id}", status_code=status.HTTP_200_OK, - response_model=SMessage + response_model=SMessage, ) -async def get_message_by_id(chat_id: int, message_id: int, user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): +async def get_message_by_id( + chat_id: int, + message_id: int, + user: SUser = Depends(check_verificated_user_with_exc), + uow=Depends(UnitOfWork) +): await AuthService.validate_user_access_to_chat(uow=uow, chat_id=chat_id, user_id=user.id) async with uow: message = await uow.chat.get_message_by_id(message_id=message_id) @@ -99,7 +122,11 @@ async def get_message_by_id(chat_id: int, message_id: int, user: SUser = Depends status_code=status.HTTP_201_CREATED, response_model=SCreateInvitationLink, ) -async def create_invitation_link(chat_id: int, user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): +async def create_invitation_link( + chat_id: int, + user: SUser = Depends(check_verificated_user_with_exc), + uow=Depends(UnitOfWork) +): await AuthService.validate_user_access_to_chat(uow=uow, chat_id=chat_id, user_id=user.id) cipher_suite = Fernet(settings.INVITATION_LINK_TOKEN_KEY) invitation_token = cipher_suite.encrypt(str(chat_id).encode()) @@ -112,7 +139,11 @@ async def create_invitation_link(chat_id: int, user: SUser = Depends(check_verif status_code=status.HTTP_200_OK, response_model=SUserAddedToChat, ) -async def invite_to_chat(invitation_token: str, user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): +async def invite_to_chat( + invitation_token: str, + user: SUser = Depends(check_verificated_user_with_exc), + uow=Depends(UnitOfWork) +): async with uow: invitation_token = invitation_token.encode() cipher_suite = Fernet(settings.INVITATION_LINK_TOKEN_KEY) @@ -139,9 +170,14 @@ async def delete_chat(chat_id: int, user: SUser = Depends(check_verificated_user @router.delete( "/delete_user_from_chat/{chat_id}", status_code=status.HTTP_200_OK, - response_model=SDeletedUser + response_model=SDeletedUser, ) -async def delete_user_from_chat(chat_id: int, user_id: int, user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): +async def delete_user_from_chat( + chat_id: int, + user_id: int, + user: SUser = Depends(check_verificated_user_with_exc), + uow=Depends(UnitOfWork) +): async with uow: chat = await uow.chat.find_one_or_none(id=chat_id) if user.id == chat.created_by: @@ -152,7 +188,7 @@ async def delete_user_from_chat(chat_id: int, user_id: int, user: SUser = Depend @router.post( "/pin_chat", status_code=status.HTTP_200_OK, - response_model=SPinnedChat + response_model=SPinnedChat, ) async def pinn_chat(chat_id: int, user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): await AuthService.validate_user_access_to_chat(uow=uow, chat_id=chat_id, user_id=user.id) @@ -164,7 +200,7 @@ async def pinn_chat(chat_id: int, user: SUser = Depends(check_verificated_user_w @router.delete( "/unpin_chat", status_code=status.HTTP_200_OK, - response_model=SPinnedChat + response_model=SPinnedChat, ) async def unpinn_chat(chat_id: int, user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): await AuthService.validate_user_access_to_chat(uow=uow, chat_id=chat_id, user_id=user.id) @@ -176,7 +212,7 @@ async def unpinn_chat(chat_id: int, user: SUser = Depends(check_verificated_user @router.get( "/get_pinned_chats", status_code=status.HTTP_200_OK, - response_model=list[SChat] + response_model=SPinnedChats, ) async def get_pinned_chats(user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): async with uow: @@ -186,7 +222,7 @@ async def get_pinned_chats(user: SUser = Depends(check_verificated_user_with_exc @router.get( "/pinned_messages/{chat_id}", status_code=status.HTTP_200_OK, - response_model=list[SMessage] | None + response_model=SPinnedMessages, ) async def pinned_messages(chat_id: int, user: SUser = Depends(check_verificated_user_with_exc), uow=Depends(UnitOfWork)): await AuthService.validate_user_access_to_chat(uow=uow, chat_id=chat_id, user_id=user.id) diff --git a/app/chat/shemas.py b/app/chat/shemas.py index a0ab21d..c8cdae0 100644 --- a/app/chat/shemas.py +++ b/app/chat/shemas.py @@ -19,6 +19,10 @@ class SMessage(BaseModel): answer_id: int | None +class SMessageList(BaseModel): + messages: list[SMessage] + + class SLastMessages(BaseModel): messages_loaded: int messages_to_get: int @@ -45,6 +49,18 @@ class SChat(BaseModel): avatar_hex: str +class SAllowedChats(BaseModel): + allowed_chats: list[SChat] + + +class SPinnedChats(BaseModel): + pinned_chat: list[SChat] + + +class SPinnedMessages(BaseModel): + pinned_messages: list[SMessage] | None + + class SSendMessage(BaseModel): flag: str message: str diff --git a/app/chat/websocket.py b/app/chat/websocket.py index b20ab31..81ee73c 100644 --- a/app/chat/websocket.py +++ b/app/chat/websocket.py @@ -117,7 +117,12 @@ manager = ConnectionManager() @router.websocket( "/ws/{chat_id}", ) -async def websocket_endpoint(chat_id: int, websocket: WebSocket, user: SUser = Depends(get_current_user_ws), uow=Depends(UnitOfWork)): +async def websocket_endpoint( + chat_id: int, + websocket: WebSocket, + user: SUser = Depends(get_current_user_ws), + uow=Depends(UnitOfWork) +): await AuthService.check_verificated_user_with_exc(uow=uow, user_id=user.id) await AuthService.validate_user_access_to_chat(uow=uow, user_id=user.id, chat_id=chat_id) await manager.connect(chat_id, websocket) diff --git a/app/dao/base.py b/app/dao/base.py index 990b9fb..0e1b8dc 100644 --- a/app/dao/base.py +++ b/app/dao/base.py @@ -1,4 +1,3 @@ -from sqlalchemy import select, insert from sqlalchemy.ext.asyncio import AsyncSession @@ -8,17 +7,3 @@ class BaseDAO: def __init__(self, session: AsyncSession): self.session = session - async def add(self, **data): # Метод добавляет данные в БД - stmt = insert(self.model).values(**data).returning(self.model.id) - result = await self.session.execute(stmt) - return result.scalar() - - async def find_one_or_none(self, **filter_by): # Метод проверяет наличие строки с заданными параметрами - query = select(self.model).filter_by(**filter_by) - result = await self.session.execute(query) - return result.scalar_one_or_none() - - async def find_all(self, **filter_by): # Метод возвращает все строки таблицы или те, которые соответствуют отбору - query = select(self.model.__table__.columns).filter_by(**filter_by) - result = await self.session.execute(query) - return result.mappings().all() diff --git a/app/chat/dao.py b/app/dao/chat.py similarity index 100% rename from app/chat/dao.py rename to app/dao/chat.py diff --git a/app/users/dao.py b/app/dao/user.py similarity index 77% rename from app/users/dao.py rename to app/dao/user.py index 2178339..6a32599 100644 --- a/app/users/dao.py +++ b/app/dao/user.py @@ -1,29 +1,42 @@ from pydantic import HttpUrl from sqlalchemy import update, select, insert, func +from sqlalchemy.exc import MultipleResultsFound, IntegrityError from app.dao.base import BaseDAO from app.database import engine # noqa +from app.exceptions import IncorrectDataException, UserAlreadyExistsException from app.models.chat import Chats from app.models.user_avatar import UserAvatar from app.models.users import Users from app.models.user_chat import UserChat -from app.users.schemas import SUser, SUserAvatars +from app.users.schemas import SUser, SUserAvatars, SUsers class UserDAO(BaseDAO): model = Users - async def find_one_or_none(self, **filter_by) -> SUser | None: - query = select(Users).filter_by(**filter_by) - result = await self.session.execute(query) - result = result.scalar_one_or_none() - if result: - return SUser.model_validate(result, from_attributes=True) + async def add(self, **data) -> int: + try: + stmt = insert(self.model).values(**data).returning(Users.id) + result = await self.session.execute(stmt) + return result.scalar() + except IntegrityError: + raise UserAlreadyExistsException - async def find_all(self, **filter_by): - query = select(Users.__table__.columns).filter_by(**filter_by).where(Users.role != 100) + async def find_one_or_none(self, **filter_by) -> SUser | None: + try: + query = select(Users.__table__.columns).filter_by(**filter_by) + result = await self.session.execute(query) + result = result.mappings().one() + if result: + return SUser.model_validate(result) + except MultipleResultsFound: + raise IncorrectDataException + + async def find_all(self) -> SUsers: + query = select(Users.__table__.columns).where(Users.role != 100) result = await self.session.execute(query) - return result.mappings().all() + return SUsers.model_validate({"users": result.mappings().all()}) async def change_data(self, user_id: int, **data_to_change) -> str: query = update(Users).where(Users.id == user_id).values(**data_to_change).returning(Users.username) diff --git a/app/exceptions.py b/app/exceptions.py index 4b55ca4..79c8f0e 100644 --- a/app/exceptions.py +++ b/app/exceptions.py @@ -14,21 +14,11 @@ class UserAlreadyExistsException(BlackPhoenixException): detail = "Пользователь с таким ником или почтой уже существует" -class UsernameAlreadyInUseException(BlackPhoenixException): - status_code = status.HTTP_409_CONFLICT - detail = "Ник занят" - - class IncorrectAuthDataException(BlackPhoenixException): status_code = status.HTTP_401_UNAUTHORIZED detail = "Введены не верные данные" -class IncorrectPasswordException(BlackPhoenixException): - status_code = status.HTTP_409_CONFLICT - detail = "Введён не верный пароль" - - class IncorrectTokenFormatException(BlackPhoenixException): status_code = status.HTTP_401_UNAUTHORIZED detail = "Некорректный формат токена" @@ -93,11 +83,6 @@ class UserMustConfirmEmailException(BlackPhoenixException): detail = "Сначала подтвердите почту" -class SomethingWentWrongException(BlackPhoenixException): - status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - detail = "Что-то пошло не так" - - class IncorrectDataException(BlackPhoenixException): status_code = status.HTTP_409_CONFLICT detail = "Ты передал какую-то хуйню" diff --git a/app/services/message_service.py b/app/services/message_service.py index 9718a56..edb98b9 100644 --- a/app/services/message_service.py +++ b/app/services/message_service.py @@ -4,7 +4,9 @@ from app.unit_of_work import UnitOfWork class MessageService: @staticmethod - async def send_message(uow: UnitOfWork, user_id: int, chat_id: int, message: str, image_url: str | None = None) -> SMessage: + async def send_message( + uow: UnitOfWork, user_id: int, chat_id: int, message: str, image_url: str | None = None + ) -> SMessage: async with uow: new_message = await uow.chat.send_message(user_id=user_id, chat_id=chat_id, message=message, image_url=image_url) return new_message @@ -24,7 +26,9 @@ class MessageService: @staticmethod async def edit_message(uow: UnitOfWork, message_id: int, new_message: str, new_image_url: str) -> bool: async with uow: - new_message = await uow.chat.edit_message(message_id=message_id, new_message=new_message, new_image_url=new_image_url) + new_message = await uow.chat.edit_message( + message_id=message_id, new_message=new_message, new_image_url=new_image_url + ) return new_message @staticmethod diff --git a/app/tasks/tasks.py b/app/tasks/tasks.py index f591ed4..b2c8bdf 100644 --- a/app/tasks/tasks.py +++ b/app/tasks/tasks.py @@ -1,9 +1,7 @@ -import json import smtplib import random import string -from cryptography.fernet import Fernet from pydantic import EmailStr @@ -14,38 +12,30 @@ from app.tasks.email_templates import ( create_password_change_confirmation_template, create_password_recover_template, ) +from app.users.auth import encode_invitation_token +from app.users.schemas import SInvitationData -def generate_confirmation_code(length=6): +def generate_confirmation_code(length=6) -> str: characters = string.ascii_letters + string.digits confirmation_code = "".join(random.choice(characters) for _ in range(length)) return confirmation_code @celery.task -def send_registration_confirmation_email(user_id: int, username: str, email_to: EmailStr, MODE: str): - confirmation_code = generate_confirmation_code() +def send_registration_confirmation_email(user_data: SInvitationData): + invitation_token = encode_invitation_token(user_data) - if MODE == "TEST": - return confirmation_code - - load = {"user_id": user_id, "username": username, "email": email_to, "confirmation_code": confirmation_code} - str_load = json.dumps(load) - cipher_suite = Fernet(settings.INVITATION_LINK_TOKEN_KEY) - invitation_token = cipher_suite.encrypt(str_load.encode()) - - confirmation_link = settings.INVITATION_LINK_HOST + "/api/users/email_verification/" + invitation_token.decode() + confirmation_link = settings.INVITATION_LINK_HOST + "/api/users/email_verification/" + invitation_token msg_content = create_registration_confirmation_template( - username=username, email_to=email_to, confirmation_link=confirmation_link + username=user_data.username, email_to=user_data.email_to, confirmation_link=confirmation_link ) with smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT) as server: server.login(settings.SMTP_USER, settings.SMTP_PASS) server.send_message(msg_content) - return confirmation_code - @celery.task def send_password_change_email(username: str, email_to: EmailStr, MODE: str): diff --git a/app/unit_of_work.py b/app/unit_of_work.py index be2f8d7..e85230e 100644 --- a/app/unit_of_work.py +++ b/app/unit_of_work.py @@ -1,6 +1,6 @@ -from app.chat.dao import ChatDAO +from app.dao.chat import ChatDAO from app.database import async_session_maker -from app.users.dao import UserDAO +from app.dao.user import UserDAO class UnitOfWork: diff --git a/app/users/auth.py b/app/users/auth.py index f1d0412..e4165ee 100644 --- a/app/users/auth.py +++ b/app/users/auth.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta, UTC +from cryptography.fernet import Fernet from jose import jwt from passlib.context import CryptContext from pydantic import EmailStr @@ -8,12 +9,11 @@ from app.config import settings from app.exceptions import ( UserDontHavePermissionException, IncorrectAuthDataException, - UserAlreadyExistsException, UserNotFoundException, UserMustConfirmEmailException, ) from app.unit_of_work import UnitOfWork -from app.users.schemas import SUserRegister, SUser +from app.users.schemas import SUser, SInvitationData pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -39,6 +39,19 @@ def create_access_token(data: dict[str, str | datetime]) -> str: return encoded_jwt +def encode_invitation_token(user_data: SInvitationData) -> str: + cipher_suite = Fernet(settings.INVITATION_LINK_TOKEN_KEY) + invitation_token = cipher_suite.encrypt(user_data.model_dump_json().encode()) + return invitation_token.decode() + + +def decode_invitation_token(invitation_token: str) -> SInvitationData: + user_code = invitation_token.encode() + cipher_suite = Fernet(settings.INVITATION_LINK_TOKEN_KEY) + user_data = cipher_suite.decrypt(user_code) + return SInvitationData.model_validate_json(user_data) + + class AuthService: @staticmethod async def authenticate_user_by_email(uow: UnitOfWork, email: EmailStr, password: str) -> SUser | None: @@ -65,16 +78,6 @@ class AuthService: raise IncorrectAuthDataException return user - @staticmethod - async def check_existing_user(uow: UnitOfWork, user_data: SUserRegister) -> None: - async with uow: - existing_user = await uow.user.find_one_or_none(email=user_data.email) - if existing_user: - raise UserAlreadyExistsException - existing_user = await uow.user.find_one_or_none(username=user_data.username) - if existing_user: - raise UserAlreadyExistsException - @staticmethod async def check_verificated_user(uow: UnitOfWork, user_id: int) -> bool: async with uow: diff --git a/app/users/router.py b/app/users/router.py index 5c4118c..92ad9fe 100644 --- a/app/users/router.py +++ b/app/users/router.py @@ -1,6 +1,3 @@ -import json - -from cryptography.fernet import Fernet from fastapi import APIRouter, Depends, status from app.config import settings @@ -12,7 +9,8 @@ from app.exceptions import ( ) from app.services.redis_service import RedisService, get_redis_session from app.unit_of_work import UnitOfWork -from app.users.auth import get_password_hash, create_access_token, VERIFICATED_USER, AuthService, verify_password +from app.users.auth import get_password_hash, create_access_token, VERIFICATED_USER, AuthService, verify_password, \ + decode_invitation_token from app.users.dependencies import get_current_user from app.users.schemas import ( SUserLogin, @@ -20,16 +18,17 @@ from app.users.schemas import ( SUserResponse, SEmailVerification, SUserAvatars, - SUsername, - SEmail, + SUserFilter, SUser, SUserChangeData, SUserSendConfirmationCode, STokenLogin, + SUsers, + SInvitationData, ) from app.tasks.tasks import ( send_registration_confirmation_email, - send_data_change_confirmation_email + send_data_change_confirmation_email, generate_confirmation_code ) router = APIRouter(prefix="/users", tags=["Пользователи"]) @@ -38,7 +37,7 @@ router = APIRouter(prefix="/users", tags=["Пользователи"]) @router.get( "", status_code=status.HTTP_200_OK, - response_model=list[SUserResponse], + response_model=SUsers, ) async def get_all_users(uow=Depends(UnitOfWork)): async with uow: @@ -47,25 +46,13 @@ async def get_all_users(uow=Depends(UnitOfWork)): @router.post( - "/check_existing_username", + "/check_existing_user", status_code=status.HTTP_200_OK, response_model=None, ) -async def check_existing_username(username: SUsername, uow=Depends(UnitOfWork)): +async def check_existing_user(user_filter: SUserFilter, uow=Depends(UnitOfWork)): async with uow: - user = await uow.user.find_one_or_none(username=username.username) - if user: - raise UserAlreadyExistsException - - -@router.post( - "/check_existing_email", - status_code=status.HTTP_200_OK, - response_model=None, -) -async def check_existing_email(email: SEmail, uow=Depends(UnitOfWork)): - async with uow: - user = await uow.user.find_one_or_none(email=email.email) + user = await uow.user.find_one_or_none(**user_filter.model_dump(exclude_none=True)) if user: raise UserAlreadyExistsException @@ -78,7 +65,6 @@ async def check_existing_email(email: SEmail, uow=Depends(UnitOfWork)): async def register_user(user_data: SUserRegister, uow=Depends(UnitOfWork)): if user_data.password != user_data.password2: raise PasswordsMismatchException - await AuthService.check_existing_user(uow, user_data) hashed_password = get_password_hash(user_data.password) async with uow: @@ -90,14 +76,14 @@ async def register_user(user_data: SUserRegister, uow=Depends(UnitOfWork)): ) await uow.commit() - result = send_registration_confirmation_email.delay( - user_id=user_id, username=user_data.username, email_to=user_data.email, MODE=settings.MODE + user_code = generate_confirmation_code() + user_mail_data = SInvitationData.model_validate( + {"user_id": user_id, "username": user_data.username, "email_to": user_data.email, "confirmation_code": user_code} ) - user_code = result.get() + send_registration_confirmation_email.delay(user_mail_data) redis_session = get_redis_session() await RedisService.set_verification_code(redis=redis_session, user_id=user_id, verification_code=user_code) - user = await AuthService.authenticate_user_by_email(uow, user_data.email, user_data.password) - access_token = create_access_token({"sub": str(user.id)}) + access_token = create_access_token({"sub": str(user_id)}) return {"authorization": f"Bearer {access_token}"} @@ -107,17 +93,14 @@ async def register_user(user_data: SUserRegister, uow=Depends(UnitOfWork)): response_model=SEmailVerification, ) async def email_verification(user_code: str, uow=Depends(UnitOfWork)): - invitation_token = user_code.encode() - cipher_suite = Fernet(settings.INVITATION_LINK_TOKEN_KEY) - user_data = cipher_suite.decrypt(invitation_token).decode("utf-8") - user_data = json.loads(user_data) + user_data = decode_invitation_token(user_code) redis_session = get_redis_session() async with uow: - verification_code = await RedisService.get_verification_code(redis=redis_session, user_id=user_data["user_id"]) - if verification_code != user_data["confirmation_code"]: + verification_code = await RedisService.get_verification_code(redis=redis_session, user_id=user_data.user_id) + if verification_code != user_data.confirmation_code: raise WrongCodeException - await uow.user.change_data(user_id=user_data["user_id"], role=VERIFICATED_USER) + await uow.user.change_data(user_id=user_data.user_id, role=VERIFICATED_USER) await uow.commit() return {"email_verification": True} diff --git a/app/users/schemas.py b/app/users/schemas.py index f32ecbb..7e83d9a 100644 --- a/app/users/schemas.py +++ b/app/users/schemas.py @@ -1,7 +1,8 @@ from datetime import date, timedelta, datetime, time +from typing import Annotated from pydantic_core import PydanticCustomError -from pydantic import BaseModel, EmailStr, ConfigDict, field_validator, HttpUrl +from pydantic import BaseModel, EmailStr, field_validator, HttpUrl, StringConstraints from fastapi import Query @@ -17,25 +18,23 @@ class SUserRegister(BaseModel): password2: str = Query(None, min_length=8) date_of_birth: date - @field_validator("date_of_birth") + @field_validator("date_of_birth") # noqa @classmethod - def validate_date_of_birth(cls, input_date): - if date.today() - input_date < timedelta(days=365 * 16): + def validate_date_of_birth(cls, v): + if date.today() - v < timedelta(days=365 * 16): date_of_birth = date.today() - timedelta(days=365 * 16) raise PydanticCustomError( "date_input_error", "date of birth might be earlier than {date_of_birth}", {"date_of_birth": date_of_birth} ) - elif datetime.combine(input_date, time.min).timestamp() < datetime(year=1924, month=1, day=1).timestamp(): + elif datetime.combine(v, time.min).timestamp() < datetime(year=1924, month=1, day=1).timestamp(): date_of_birth = date(1924, 1, 1) raise PydanticCustomError( "date_input_error", "date of birth might be later than {date_of_birth}", {"date_of_birth": date_of_birth} ) - return input_date + return v class SUserResponse(BaseModel): - model_config = ConfigDict(from_attributes=True) - email: EmailStr id: int username: str @@ -46,6 +45,10 @@ class SUserResponse(BaseModel): date_of_registration: date +class SUsers(BaseModel): + users: list[SUserResponse] + + class SUser(BaseModel): id: int email: str @@ -96,9 +99,13 @@ class SUserAvatars(BaseModel): user_avatars: list[SUserAvatarUrl] | None -class SUsername(BaseModel): - username: str = Query(None, min_length=2, max_length=30) +class SUserFilter(BaseModel): + username: Annotated[str, StringConstraints(min_length=2, max_length=30)] | None = None + email: EmailStr | None = None -class SEmail(BaseModel): - email: EmailStr +class SInvitationData(BaseModel): + user_id: int + username: str + email_to: EmailStr + confirmation_code: str