Добавлена админка
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 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")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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