Добавлена админка
This commit is contained in:
parent
94cd0e36a6
commit
70653e71e2
11 changed files with 131 additions and 123 deletions
42
chat_test/app/admin/auth.py
Normal file
42
chat_test/app/admin/auth.py
Normal 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)
|
54
chat_test/app/admin/views.py
Normal file
54
chat_test/app/admin/views.py
Normal 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"
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
Generic single-database configuration.
|
|
@ -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()
|
|
@ -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"}
|
Loading…
Add table
Reference in a new issue