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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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