From c63dc171bce4dd755934b4ca242698265516ef5e Mon Sep 17 00:00:00 2001 From: urec56 Date: Mon, 29 Jan 2024 22:42:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F,=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=8E=D0=B7=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/dao/base.py | 23 ++++- app/exceptions.py | 56 ++++++++++++ app/main.py | 2 + .../3eb642de804e_убрал_обязательность_авы.py | 32 +++++++ .../43eac1ddf80a_добавил_колонку_юзерам.py | 30 +++++++ .../90665e133296_добавил_колонку_юзерам.py | 30 +++++++ app/users/auth.py | 16 +++- app/users/chat/dao.py | 7 ++ app/users/dao.py | 21 +++++ app/users/dependencies.py | 36 ++++++++ app/users/models.py | 5 +- app/users/router.py | 86 +++++++++++++++++++ app/users/schemas.py | 22 +++++ requirements.txt | 4 +- 14 files changed, 363 insertions(+), 7 deletions(-) create mode 100644 app/exceptions.py create mode 100644 app/migrations/versions/3eb642de804e_убрал_обязательность_авы.py create mode 100644 app/migrations/versions/43eac1ddf80a_добавил_колонку_юзерам.py create mode 100644 app/migrations/versions/90665e133296_добавил_колонку_юзерам.py create mode 100644 app/users/chat/dao.py create mode 100644 app/users/dao.py create mode 100644 app/users/dependencies.py create mode 100644 app/users/router.py create mode 100644 app/users/schemas.py diff --git a/app/dao/base.py b/app/dao/base.py index d911703..56fb871 100644 --- a/app/dao/base.py +++ b/app/dao/base.py @@ -1,7 +1,28 @@ -from sqlalchemy import insert, select +from sqlalchemy import select, insert from app.database import async_session_maker class BaseDAO: model = None + + @classmethod + async def add(cls, **data): # Метод добавляет данные в БД + async with async_session_maker() as session: + query = insert(cls.model).values(**data) + await session.execute(query) + await session.commit() + + @classmethod + async def find_one_or_none(cls, **filter_by): # Метод проверяет наличие строки с заданными параметрами + async with async_session_maker() as session: + query = select(cls.model).filter_by(**filter_by) + result = await session.execute(query) + return result.scalar_one_or_none() + + @classmethod + async def find_all(cls, **filter_by): # Метод возвращает все строки таблицы или те, которые соответствуют отбору + async with async_session_maker() as session: + query = select(cls.model).filter_by(**filter_by) + result = await session.execute(query) + return result.mappings().all() diff --git a/app/exceptions.py b/app/exceptions.py new file mode 100644 index 0000000..53ec607 --- /dev/null +++ b/app/exceptions.py @@ -0,0 +1,56 @@ +from fastapi import HTTPException, status + + +# Базовое исключение +class BlackPhoenixException(HTTPException): + status_code = 500 + detail = "" + + def __init__(self): + super().__init__(status_code=self.status_code, detail=self.detail) + + +class UserAlreadyExistsException(BlackPhoenixException): + status_code = status.HTTP_409_CONFLICT + detail = "Пользователь с таким ником или почтой уже существует" + + +class UsernameAlreadyInUseException(BlackPhoenixException): + status_code = status.HTTP_409_CONFLICT + detail = "Ник занят" + + +class IncorrectAuthDataException(BlackPhoenixException): + status_code = status.HTTP_401_UNAUTHORIZED + detail = "Введены не верные данные" + + +class IncorrectTokenFormatException(BlackPhoenixException): + status_code = status.HTTP_401_UNAUTHORIZED + detail = "Некорректный формат токена" + + +class TokenAbsentException(BlackPhoenixException): + status_code = status.HTTP_401_UNAUTHORIZED + detail = "Токен отсутствует" + + +class TokenExpiredException(BlackPhoenixException): + status_code = status.HTTP_401_UNAUTHORIZED + detail = "Токен истёк" + + +class UserIsNotPresentException(BlackPhoenixException): + status_code = status.HTTP_401_UNAUTHORIZED + + +class PasswordIsTooShortException(BlackPhoenixException): + status_code = status.HTTP_409_CONFLICT + detail = "Пароль должен быть не менее 8 символов" + + +class IncorrectLengthOfNicknameException(BlackPhoenixException): + status_code = status.HTTP_409_CONFLICT + detail = "Ник должен быть не короче 2 и не длиннее 30 символов" + + diff --git a/app/main.py b/app/main.py index 0439a63..a4c1df7 100644 --- a/app/main.py +++ b/app/main.py @@ -1,10 +1,12 @@ from fastapi import FastAPI from app.users.chat.router import router as chat_router +from app.users.router import router as user_router app = FastAPI() app.include_router(chat_router) +app.include_router(user_router) @app.get('/') diff --git a/app/migrations/versions/3eb642de804e_убрал_обязательность_авы.py b/app/migrations/versions/3eb642de804e_убрал_обязательность_авы.py new file mode 100644 index 0000000..5ca58a0 --- /dev/null +++ b/app/migrations/versions/3eb642de804e_убрал_обязательность_авы.py @@ -0,0 +1,32 @@ +"""Убрал обязательность авы + +Revision ID: 3eb642de804e +Revises: 43eac1ddf80a +Create Date: 2024-01-29 19:37:41.136288 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '3eb642de804e' +down_revision: Union[str, None] = '43eac1ddf80a' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('avatar_image', sa.String(), nullable=True)) + op.drop_column('users', 'image') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('image', sa.VARCHAR(), autoincrement=False, nullable=False)) + op.drop_column('users', 'avatar_image') + # ### end Alembic commands ### diff --git a/app/migrations/versions/43eac1ddf80a_добавил_колонку_юзерам.py b/app/migrations/versions/43eac1ddf80a_добавил_колонку_юзерам.py new file mode 100644 index 0000000..6e28a93 --- /dev/null +++ b/app/migrations/versions/43eac1ddf80a_добавил_колонку_юзерам.py @@ -0,0 +1,30 @@ +"""Добавил колонку юзерам + +Revision ID: 43eac1ddf80a +Revises: 90665e133296 +Create Date: 2024-01-29 19:35:47.727712 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '43eac1ddf80a' +down_revision: Union[str, None] = '90665e133296' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('date_of_birth', sa.Date(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'date_of_birth') + # ### end Alembic commands ### diff --git a/app/migrations/versions/90665e133296_добавил_колонку_юзерам.py b/app/migrations/versions/90665e133296_добавил_колонку_юзерам.py new file mode 100644 index 0000000..4ac8986 --- /dev/null +++ b/app/migrations/versions/90665e133296_добавил_колонку_юзерам.py @@ -0,0 +1,30 @@ +"""Добавил колонку юзерам + +Revision ID: 90665e133296 +Revises: 9845ad4fed24 +Create Date: 2024-01-29 18:50:09.853356 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '90665e133296' +down_revision: Union[str, None] = '9845ad4fed24' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('image', sa.String(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'image') + # ### end Alembic commands ### diff --git a/app/users/auth.py b/app/users/auth.py index e9195a9..7afa2ae 100644 --- a/app/users/auth.py +++ b/app/users/auth.py @@ -5,7 +5,7 @@ from passlib.context import CryptContext from pydantic import EmailStr from app.config import settings -from app.users.dao import UsersDAO +from app.users.dao import UserDAO pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -30,8 +30,18 @@ def create_access_token(data: dict) -> str: # Функция проверки наличия юзера -async def authenticate_user(email: EmailStr, password: str): - user = await UsersDAO.find_one_or_none(email=email) +async def authenticate_user_by_email(email: EmailStr, password: str): + user = await UserDAO.find_one_or_none(email=email) if not user or not verify_password(password, user.hashed_password): return None return user + + +async def authenticate_user_by_username(username: str, password: str): + user = await UserDAO.find_one_or_none(username=username) + if not user or not verify_password(password, user.hashed_password): + return None + return user + + + diff --git a/app/users/chat/dao.py b/app/users/chat/dao.py new file mode 100644 index 0000000..854f600 --- /dev/null +++ b/app/users/chat/dao.py @@ -0,0 +1,7 @@ +from app.dao.base import BaseDAO +from app.users.models import Users +from app.users.chat.models import Chats + + +class ChatDAO(BaseDAO): + model = Chats diff --git a/app/users/dao.py b/app/users/dao.py new file mode 100644 index 0000000..abdfdc2 --- /dev/null +++ b/app/users/dao.py @@ -0,0 +1,21 @@ +from sqlalchemy import update, select +from sqlalchemy.exc import SQLAlchemyError + +from app.dao.base import BaseDAO +from app.database import async_session_maker +from app.users.models import Users + + +class UserDAO(BaseDAO): + model = Users + + @classmethod + async def change_data(cls, user_id: int, **data_to_change): + query = update(Users).where(Users.id == user_id).values(**data_to_change) + async with async_session_maker() as session: + await session.execute(query) + await session.commit() + query = select(Users.username).where(Users.id == user_id) + result = await session.execute(query) + return result.scalar() + diff --git a/app/users/dependencies.py b/app/users/dependencies.py new file mode 100644 index 0000000..bcac202 --- /dev/null +++ b/app/users/dependencies.py @@ -0,0 +1,36 @@ +from datetime import datetime + +from fastapi import Depends, Request +from jose import JWTError, jwt + +from app.config import settings +from app.exceptions import (IncorrectTokenFormatException, + TokenAbsentException, TokenExpiredException, + UserIsNotPresentException) +from app.users.dao import UserDAO + + +def get_token(request: Request): + token = request.cookies.get("booking_access_token") + if not token: + raise TokenAbsentException + return token + + +async def get_current_user(token: str = Depends(get_token)): + try: + payload = jwt.decode( + token, settings.SECRET_KEY, settings.ALGORITHM + ) + except JWTError: + raise IncorrectTokenFormatException + expire: str = payload.get("exp") + if not expire or int(expire) < datetime.utcnow().timestamp(): + raise TokenExpiredException + user_id: str = payload.get("sub") + if not user_id: + raise UserIsNotPresentException + user = await UserDAO.find_one_or_none(id=int(user_id)) + if not user: + raise UserIsNotPresentException + return user diff --git a/app/users/models.py b/app/users/models.py index 4d4831c..276b96f 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -1,5 +1,4 @@ -from sqlalchemy import Column, Integer, String - +from sqlalchemy import Column, Integer, String, Date from app.database import Base @@ -13,6 +12,8 @@ class Users(Base): hashed_password = Column(String, nullable=False) role = Column(Integer, nullable=False) black_phoenix = Column(Integer, nullable=False) + avatar_image = Column(String) + date_of_birth = Column(Date, nullable=False) def __str__(self): return f"Юзер {self.username}" diff --git a/app/users/router.py b/app/users/router.py new file mode 100644 index 0000000..a3fcc5b --- /dev/null +++ b/app/users/router.py @@ -0,0 +1,86 @@ +from fastapi import APIRouter, Response, Depends +from pydantic import EmailStr + +from app.exceptions import UserAlreadyExistsException, IncorrectAuthDataException, UsernameAlreadyInUseException, \ + PasswordIsTooShortException, IncorrectLengthOfNicknameException +from app.users.auth import get_password_hash, authenticate_user_by_email, authenticate_user_by_username, \ + create_access_token +from app.users.dao import UserDAO +from app.users.dependencies import get_current_user +from app.users.models import Users +from app.users.schemas import SUserLogin, SUserRegister + +router = APIRouter( + prefix="/users", + tags=["Пользователи"] +) + + +@router.get("") +async def get_all_users(): + users = await UserDAO.find_all() + return users + + +@router.post("/register") +async def register_user(response: Response, user_data: SUserRegister): + existing_user = await UserDAO.find_one_or_none(email=user_data.email) + if existing_user: + raise UserAlreadyExistsException + existing_user = await UserDAO.find_one_or_none(username=user_data.username) + if existing_user: + raise UserAlreadyExistsException + if len(user_data.password) < 8: + raise PasswordIsTooShortException + if len(user_data.username) < 2 or len(user_data.username) > 30: + raise IncorrectLengthOfNicknameException + hashed_password = get_password_hash(user_data.password) + await UserDAO.add(email=user_data.email, hashed_password=hashed_password, + username=user_data.username, date_of_birth=user_data.date_of_birth, + role=0, black_phoenix=0) + user = await authenticate_user_by_email(user_data.email, user_data.password) + access_token = create_access_token({"sub": str(user.id)}) + response.set_cookie("booking_access_token", access_token, httponly=True) + return {"access_token": access_token} + + +@router.post("/login") +async def login_user(response: Response, user_data: SUserLogin): + user = await authenticate_user_by_email(user_data.email_or_username, user_data.password) + if not user: + user = await authenticate_user_by_username(user_data.email_or_username, user_data.password) + if not user: + raise IncorrectAuthDataException + access_token = create_access_token({"sub": str(user.id)}) + response.set_cookie("booking_access_token", access_token, httponly=True) + return {"access_token": access_token} + + +@router.post("/logout") +async def logout_user(response: Response): + response.delete_cookie("booking_access_token") + + +@router.get("/me") +async def read_users_me(current_user: Users = Depends(get_current_user)): + return current_user + + +@router.patch("/rename") +async def rename_user(new_username, current_user: Users = Depends(get_current_user)): + if len(new_username) < 2 or len(new_username) > 30: + raise IncorrectLengthOfNicknameException + existing_user = await UserDAO.find_one_or_none(username=new_username) + if existing_user: + raise UsernameAlreadyInUseException + new_username = await UserDAO.change_data(current_user.id, username=new_username) + return new_username + + +@router.patch("/change_password") +async def change_password(new_password, current_user: Users = Depends(get_current_user)): + if len(new_password) < 8: + raise PasswordIsTooShortException + hashed_password = get_password_hash(new_password) + await UserDAO.change_data(current_user.id, hashed_password=hashed_password) + diff --git a/app/users/schemas.py b/app/users/schemas.py new file mode 100644 index 0000000..a129345 --- /dev/null +++ b/app/users/schemas.py @@ -0,0 +1,22 @@ +from datetime import date + +from pydantic import BaseModel, EmailStr + + +class SUserLogin(BaseModel): + email_or_username: EmailStr | str + password: str + + class Config: + orm_mode = True + + +class SUserRegister(BaseModel): + email: EmailStr + username: str + password: str + date_of_birth: date + + class Config: + orm_mode = True + diff --git a/requirements.txt b/requirements.txt index da94079..ed6d0e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,6 @@ async-timeout==4.0.3 asyncpg==0.29.0 attrs==23.2.0 autoflake==2.2.1 -bcrypt==4.1.2 billiard==4.2.0 black==23.12.1 celery==5.3.6 @@ -27,6 +26,7 @@ fastapi-versioning==0.10.0 flake8==7.0.0 flower==2.0.1 frozenlist==1.4.1 +gevent==23.9.1 greenlet==3.0.3 gunicorn==21.2.0 h11==0.14.0 @@ -95,3 +95,5 @@ wcwidth==0.2.13 websockets==12.0 WTForms==3.1.2 yarl==1.9.4 +zope.event==5.0 +zope.interface==6.1