Добавлена регистрация, авторизация юзеров

This commit is contained in:
urec56 2024-01-29 22:42:06 +03:00
parent 93b4f15a95
commit c63dc171bc
14 changed files with 363 additions and 7 deletions

View file

@ -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()

56
app/exceptions.py Normal file
View file

@ -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 символов"

View file

@ -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('/')

View file

@ -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 ###

View file

@ -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 ###

View file

@ -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 ###

View file

@ -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

7
app/users/chat/dao.py Normal file
View file

@ -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

21
app/users/dao.py Normal file
View file

@ -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()

36
app/users/dependencies.py Normal file
View file

@ -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

View file

@ -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}"

86
app/users/router.py Normal file
View file

@ -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)

22
app/users/schemas.py Normal file
View file

@ -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

View file

@ -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