Добавлена админка

This commit is contained in:
urec56 2024-02-10 15:47:18 +03:00
parent 94cd0e36a6
commit 70653e71e2
11 changed files with 131 additions and 123 deletions

View file

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

View file

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

View file

@ -1,7 +1,11 @@
from fastapi import FastAPI from fastapi import FastAPI
from sqladmin import Admin
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware 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.chat.router import router as chat_router
from app.users.router import router as user_router from app.users.router import router as user_router
from app.pages.router import router as pages_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") app.mount("/static", StaticFiles(directory="app/static"), name="static")

View file

@ -5,7 +5,9 @@ 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.exceptions import UserDontHavePermissionException
from app.users.dao import UserDAO from app.users.dao import UserDAO
from app.users.models import Users
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@ -29,16 +31,29 @@ def create_access_token(data: dict) -> str:
return encoded_jwt 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) 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): # Функция проверки наличия юзера по нику
async def authenticate_user_by_username(username: str, password: str) -> Users | None:
user = await UserDAO.find_one_or_none(username=username) user = await UserDAO.find_one_or_none(username=username)
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 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

View file

@ -36,7 +36,7 @@ class Messages(Base):
user = relationship("Users", back_populates="message") user = relationship("Users", back_populates="message")
def __str__(self): 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): class UsersXChats(Base):

View file

@ -5,7 +5,8 @@ from app.users.chat.dao import ChatDAO
from app.users.chat.shemas import SMessage, SLastMessages from app.users.chat.shemas import SMessage, SLastMessages
from app.users.dao import UserDAO 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 from app.users.models import Users
router = APIRouter( router = APIRouter(

View file

@ -3,7 +3,7 @@ from typing import Dict, List
from fastapi import WebSocket, Depends, WebSocketDisconnect from fastapi import WebSocket, Depends, WebSocketDisconnect
from app.users.chat.dao import ChatDAO 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.models import Users
from app.users.chat.router import router from app.users.chat.router import router

View file

@ -6,7 +6,7 @@ from jose import JWTError, jwt
from app.config import settings from app.config import settings
from app.exceptions import (IncorrectTokenFormatException, from app.exceptions import (IncorrectTokenFormatException,
TokenAbsentException, TokenExpiredException, TokenAbsentException, TokenExpiredException,
UserIsNotPresentException, UserDontHavePermissionException) UserIsNotPresentException)
from app.users.dao import UserDAO from app.users.dao import UserDAO
from app.users.models import Users from app.users.models import Users
@ -18,7 +18,7 @@ def get_token(request: Request):
return token return token
async def get_current_user(token: str = Depends(get_token)): async def get_current_user(token: str = Depends(get_token)) -> Users:
try: try:
payload = jwt.decode( payload = jwt.decode(
token, settings.SECRET_KEY, settings.ALGORITHM token, settings.SECRET_KEY, settings.ALGORITHM
@ -37,13 +37,3 @@ async def get_current_user(token: str = Depends(get_token)):
return user 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

View file

@ -1 +0,0 @@
Generic single-database configuration.

View file

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

View file

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