From 70653e71e2f6c91a3141f5c1a3787226fc93096b Mon Sep 17 00:00:00 2001 From: urec56 Date: Sat, 10 Feb 2024 15:47:18 +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=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat_test/app/admin/auth.py | 42 +++++++++++++++ chat_test/app/admin/views.py | 54 +++++++++++++++++++ chat_test/app/main.py | 11 ++++ chat_test/app/users/auth.py | 21 ++++++-- chat_test/app/users/chat/models.py | 2 +- chat_test/app/users/chat/router.py | 3 +- chat_test/app/users/chat/websocket.py | 2 +- chat_test/app/users/dependencies.py | 14 +---- chat_test/migrations/README | 1 - chat_test/migrations/env.py | 78 --------------------------- chat_test/migrations/script.py.mako | 26 --------- 11 files changed, 131 insertions(+), 123 deletions(-) create mode 100644 chat_test/app/admin/auth.py create mode 100644 chat_test/app/admin/views.py delete mode 100644 chat_test/migrations/README delete mode 100644 chat_test/migrations/env.py delete mode 100644 chat_test/migrations/script.py.mako diff --git a/chat_test/app/admin/auth.py b/chat_test/app/admin/auth.py new file mode 100644 index 0000000..30691e8 --- /dev/null +++ b/chat_test/app/admin/auth.py @@ -0,0 +1,42 @@ +from sqladmin.authentication import AuthenticationBackend +from starlette.requests import Request + +from app.config import settings +from app.users.auth import authenticate_user_by_username, create_access_token +from app.users.dependencies import get_current_user + +ADMIN_ROLE = 100 + + +class AdminAuth(AuthenticationBackend): + async def login(self, request: Request) -> bool: + form = await request.form() + username, password = form["username"], form["password"] + + user = await authenticate_user_by_username(username, password) + if user and user.role == ADMIN_ROLE: + access_token = create_access_token({"sub": str(user.id)}) + request.session.update({"token": access_token}) + + return True + + async def logout(self, request: Request) -> bool: + # Usually you'd want to just clear the session + request.session.clear() + return True + + async def authenticate(self, request: Request) -> bool: + token = request.session.get("token") + + if not token: + return False + + user = await get_current_user(token) + if not user or user.role != ADMIN_ROLE: + return False + + # Check the token in depth + return True + + +authentication_backend = AdminAuth(secret_key=settings.SECRET_KEY) diff --git a/chat_test/app/admin/views.py b/chat_test/app/admin/views.py new file mode 100644 index 0000000..5b6703b --- /dev/null +++ b/chat_test/app/admin/views.py @@ -0,0 +1,54 @@ +from sqladmin import ModelView + +from app.users.models import Users +from app.users.chat.models import Chats, UsersXChats, Messages + + +class UsersAdmin(ModelView, model=Users): + column_list = [ + Users.id, + Users.email, + Users.username, + Users.role, + Users.black_phoenix, + Users.avatar_image, + Users.date_of_birth, + Users.date_of_registration, + Users.usersxchats, + ] + + column_details_exclude_list = [Users.hashed_password] + can_delete = False + name = "Пользователь" + name_plural = "Пользователи" + icon = "fa-solid fa-users" + + +class ChatsAdmin(ModelView, model=Chats): + column_list = [Chats.id, Chats.chat_for, Chats.usersxchats] + name = "Чат" + name_plural = "Чаты" + icon = "fa-solid fa-comment" + + +class MessagesAdmin(ModelView, model=Messages): + column_list = [ + Messages.id, + Messages.chat_id, + Messages.user_id, + Messages.message, + Messages.image_url, + Messages.created_at, + Messages.visibility, + Messages.user, + ] + name = "Сообщение" + name_plural = "Сообщения" + icon = "fa-solid fa-sms" + + +class UsersXChatsAdmin(ModelView, model=UsersXChats): + column_list = [UsersXChats.user_id, UsersXChats.chat_id] + name = "Допущенный чат" + name_plural = "Допущенные чаты" + icon = "fa-solid fa-list" diff --git a/chat_test/app/main.py b/chat_test/app/main.py index 8933fc1..cefc42f 100644 --- a/chat_test/app/main.py +++ b/chat_test/app/main.py @@ -1,7 +1,11 @@ from fastapi import FastAPI +from sqladmin import Admin from starlette.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware +from app.admin.auth import authentication_backend +from app.admin.views import UsersAdmin, ChatsAdmin, MessagesAdmin, UsersXChatsAdmin +from app.database import engine from app.users.chat.router import router as chat_router from app.users.router import router as user_router from app.pages.router import router as pages_router @@ -33,6 +37,13 @@ app.add_middleware( ], ) +admin = Admin(app, engine, authentication_backend=authentication_backend) + +admin.add_view(UsersAdmin) +admin.add_view(ChatsAdmin) +admin.add_view(MessagesAdmin) +admin.add_view(UsersXChatsAdmin) + app.mount("/static", StaticFiles(directory="app/static"), name="static") diff --git a/chat_test/app/users/auth.py b/chat_test/app/users/auth.py index defe906..b25bf2b 100644 --- a/chat_test/app/users/auth.py +++ b/chat_test/app/users/auth.py @@ -5,7 +5,9 @@ from passlib.context import CryptContext from pydantic import EmailStr from app.config import settings +from app.exceptions import UserDontHavePermissionException from app.users.dao import UserDAO +from app.users.models import Users pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -29,16 +31,29 @@ def create_access_token(data: dict) -> str: return encoded_jwt -# Функция проверки наличия юзера -async def authenticate_user_by_email(email: EmailStr, password: str): +# Функция проверки наличия юзера по мейлу +async def authenticate_user_by_email(email: EmailStr, password: str) -> Users | None: 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): +# Функция проверки наличия юзера по нику +async def authenticate_user_by_username(username: str, password: str) -> Users | None: user = await UserDAO.find_one_or_none(username=username) if not user or not verify_password(password, user.hashed_password): return None return user + + +async def get_user_allowed_chats(user_id: int): + user_allowed_chats = await UserDAO.get_user_allowed_chats(user_id) + return user_allowed_chats + + +async def validate_user_access_to_chat(user_id: int, chat_id: int): + user_allowed_chats = await get_user_allowed_chats(user_id=user_id) + if not chat_id in user_allowed_chats: + raise UserDontHavePermissionException + return True diff --git a/chat_test/app/users/chat/models.py b/chat_test/app/users/chat/models.py index 022a7dd..5392085 100644 --- a/chat_test/app/users/chat/models.py +++ b/chat_test/app/users/chat/models.py @@ -36,7 +36,7 @@ class Messages(Base): user = relationship("Users", back_populates="message") def __str__(self): - return f"#{self.id} {self.text} от {self.user_id}. Написано {self.created_at}" + return f"#{self.id} {self.message} от {self.user_id}. Написано {self.created_at}" class UsersXChats(Base): diff --git a/chat_test/app/users/chat/router.py b/chat_test/app/users/chat/router.py index 849411e..03b2b61 100644 --- a/chat_test/app/users/chat/router.py +++ b/chat_test/app/users/chat/router.py @@ -5,7 +5,8 @@ from app.users.chat.dao import ChatDAO from app.users.chat.shemas import SMessage, SLastMessages from app.users.dao import UserDAO -from app.users.dependencies import get_current_user, validate_user_access_to_chat +from app.users.dependencies import get_current_user +from app.users.auth import validate_user_access_to_chat from app.users.models import Users router = APIRouter( diff --git a/chat_test/app/users/chat/websocket.py b/chat_test/app/users/chat/websocket.py index d64a30f..620af98 100644 --- a/chat_test/app/users/chat/websocket.py +++ b/chat_test/app/users/chat/websocket.py @@ -3,7 +3,7 @@ from typing import Dict, List from fastapi import WebSocket, Depends, WebSocketDisconnect from app.users.chat.dao import ChatDAO -from app.users.dependencies import validate_user_access_to_chat, get_current_user +from app.users.auth import validate_user_access_to_chat from app.users.models import Users from app.users.chat.router import router diff --git a/chat_test/app/users/dependencies.py b/chat_test/app/users/dependencies.py index 5956637..782ad13 100644 --- a/chat_test/app/users/dependencies.py +++ b/chat_test/app/users/dependencies.py @@ -6,7 +6,7 @@ from jose import JWTError, jwt from app.config import settings from app.exceptions import (IncorrectTokenFormatException, TokenAbsentException, TokenExpiredException, - UserIsNotPresentException, UserDontHavePermissionException) + UserIsNotPresentException) from app.users.dao import UserDAO from app.users.models import Users @@ -18,7 +18,7 @@ def get_token(request: Request): return token -async def get_current_user(token: str = Depends(get_token)): +async def get_current_user(token: str = Depends(get_token)) -> Users: try: payload = jwt.decode( token, settings.SECRET_KEY, settings.ALGORITHM @@ -37,13 +37,3 @@ async def get_current_user(token: str = Depends(get_token)): return user -async def get_user_allowed_chats(user_id: int): - user_allowed_chats = await UserDAO.get_user_allowed_chats(user_id) - return user_allowed_chats - - -async def validate_user_access_to_chat(user_id: int, chat_id: int): - user_allowed_chats = await get_user_allowed_chats(user_id=user_id) - if not chat_id in user_allowed_chats: - raise UserDontHavePermissionException - return True diff --git a/chat_test/migrations/README b/chat_test/migrations/README deleted file mode 100644 index 98e4f9c..0000000 --- a/chat_test/migrations/README +++ /dev/null @@ -1 +0,0 @@ -Generic single-database configuration. \ No newline at end of file diff --git a/chat_test/migrations/env.py b/chat_test/migrations/env.py deleted file mode 100644 index 36112a3..0000000 --- a/chat_test/migrations/env.py +++ /dev/null @@ -1,78 +0,0 @@ -from logging.config import fileConfig - -from sqlalchemy import engine_from_config -from sqlalchemy import pool - -from alembic import context - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -target_metadata = None - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - connectable = engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - poolclass=pool.NullPool, - ) - - with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) - - with context.begin_transaction(): - context.run_migrations() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/chat_test/migrations/script.py.mako b/chat_test/migrations/script.py.mako deleted file mode 100644 index fbc4b07..0000000 --- a/chat_test/migrations/script.py.mako +++ /dev/null @@ -1,26 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: Union[str, None] = ${repr(down_revision)} -branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} -depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"}