Добавлена регистрация, авторизация юзеров
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
|
from app.database import async_session_maker
|
||||||
|
|
||||||
|
|
||||||
class BaseDAO:
|
class BaseDAO:
|
||||||
model = None
|
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 fastapi import FastAPI
|
||||||
|
|
||||||
from app.users.chat.router import router as chat_router
|
from app.users.chat.router import router as chat_router
|
||||||
|
from app.users.router import router as user_router
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
app.include_router(chat_router)
|
app.include_router(chat_router)
|
||||||
|
app.include_router(user_router)
|
||||||
|
|
||||||
|
|
||||||
@app.get('/')
|
@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 pydantic import EmailStr
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.users.dao import UsersDAO
|
from app.users.dao import UserDAO
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
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):
|
async def authenticate_user_by_email(email: EmailStr, password: str):
|
||||||
user = await UsersDAO.find_one_or_none(email=email)
|
user = await UserDAO.find_one_or_none(email=email)
|
||||||
if not user or not verify_password(password, user.hashed_password):
|
if not user or not verify_password(password, user.hashed_password):
|
||||||
return None
|
return None
|
||||||
return user
|
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
|
from app.database import Base
|
||||||
|
|
||||||
|
@ -13,6 +12,8 @@ class Users(Base):
|
||||||
hashed_password = Column(String, nullable=False)
|
hashed_password = Column(String, nullable=False)
|
||||||
role = Column(Integer, nullable=False)
|
role = Column(Integer, nullable=False)
|
||||||
black_phoenix = Column(Integer, nullable=False)
|
black_phoenix = Column(Integer, nullable=False)
|
||||||
|
avatar_image = Column(String)
|
||||||
|
date_of_birth = Column(Date, nullable=False)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Юзер {self.username}"
|
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
|
asyncpg==0.29.0
|
||||||
attrs==23.2.0
|
attrs==23.2.0
|
||||||
autoflake==2.2.1
|
autoflake==2.2.1
|
||||||
bcrypt==4.1.2
|
|
||||||
billiard==4.2.0
|
billiard==4.2.0
|
||||||
black==23.12.1
|
black==23.12.1
|
||||||
celery==5.3.6
|
celery==5.3.6
|
||||||
|
@ -27,6 +26,7 @@ fastapi-versioning==0.10.0
|
||||||
flake8==7.0.0
|
flake8==7.0.0
|
||||||
flower==2.0.1
|
flower==2.0.1
|
||||||
frozenlist==1.4.1
|
frozenlist==1.4.1
|
||||||
|
gevent==23.9.1
|
||||||
greenlet==3.0.3
|
greenlet==3.0.3
|
||||||
gunicorn==21.2.0
|
gunicorn==21.2.0
|
||||||
h11==0.14.0
|
h11==0.14.0
|
||||||
|
@ -95,3 +95,5 @@ wcwidth==0.2.13
|
||||||
websockets==12.0
|
websockets==12.0
|
||||||
WTForms==3.1.2
|
WTForms==3.1.2
|
||||||
yarl==1.9.4
|
yarl==1.9.4
|
||||||
|
zope.event==5.0
|
||||||
|
zope.interface==6.1
|
||||||
|
|
Loading…
Add table
Reference in a new issue