Добавлена регистрация, авторизация юзеров
This commit is contained in:
parent
93b4f15a95
commit
c63dc171bc
14 changed files with 363 additions and 7 deletions
|
@ -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
56
app/exceptions.py
Normal 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 символов"
|
||||
|
||||
|
|
@ -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('/')
|
||||
|
|
|
@ -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 ###
|
|
@ -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 ###
|
|
@ -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 ###
|
|
@ -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
7
app/users/chat/dao.py
Normal 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
21
app/users/dao.py
Normal 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
36
app/users/dependencies.py
Normal 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
|
|
@ -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
86
app/users/router.py
Normal 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
22
app/users/schemas.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue