add init commit

This commit is contained in:
harold 2025-08-28 11:34:22 +05:00
parent 330d39373f
commit 294c0ebb2d
17 changed files with 513 additions and 59 deletions

213
.gitignore vendored
View File

@ -1 +1,212 @@
.env
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
*__pycache
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
.vscode
.env
.idea/
.venv

37
Dockerfile Normal file
View File

@ -0,0 +1,37 @@
FROM python:3.13-alpine3.22 as base
ENV VENV_PATH=/app/.venv
ENV PATH="$VENV_PATH/bin:$PATH"
WORKDIR /app
FROM base as builder
ENV POETRY_VERSION=2.0.1
ENV POETRY_VIRTUALENVS_IN_PROJECT=1
ENV POETRY_NO_INTERACTION=1
ENV POETRY_VIRTUALENVS_CREATE=1
RUN pip install poetry=="$POETRY_VERSION"
COPY pyproject.toml poetry.lock ./
RUN poetry install --no-root
FROM base as runtime
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app
RUN mkdir -p /tmp/metrics
ENV PROMETHEUS_MULTIPROC_DIR=/tmp/metrics
COPY --from=builder "$VENV_PATH" "$VENV_PATH"
EXPOSE 80
COPY src ./src

18
docker-compose.yml Normal file
View File

@ -0,0 +1,18 @@
version: "3.9"
services:
ultrazord-api-dev:
build:
dockerfile: Dockerfile
target: runtime
entrypoint: >
uvicorn src.main:app
--reload
--port 80
--host 0.0.0.0
--forwarded-allow-ips='*'
--proxy-headers
env_file: .env
ports:
- "31006:80"

94
poetry.lock generated
View File

@ -124,6 +124,25 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "ecdsa"
version = "0.19.1"
description = "ECDSA cryptographic signature library (pure python)"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6"
groups = ["main"]
files = [
{file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"},
{file = "ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"},
]
[package.dependencies]
six = ">=1.9.0"
[package.extras]
gmpy = ["gmpy"]
gmpy2 = ["gmpy2"]
[[package]]
name = "fastapi"
version = "0.116.1"
@ -242,6 +261,18 @@ files = [
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "itsdangerous"
version = "2.2.0"
description = "Safely pass data to untrusted environments and back."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
]
[[package]]
name = "jinja2"
version = "3.1.6"
@ -331,6 +362,18 @@ files = [
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
]
[[package]]
name = "pyasn1"
version = "0.6.1"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"},
{file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"},
]
[[package]]
name = "pydantic"
version = "2.11.7"
@ -480,6 +523,29 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-jose"
version = "3.5.0"
description = "JOSE implementation in Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771"},
{file = "python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b"},
]
[package.dependencies]
ecdsa = "!=0.15"
pyasn1 = ">=0.5.0"
rsa = ">=4.0,<4.1.1 || >4.1.1,<4.4 || >4.4,<5.0"
[package.extras]
cryptography = ["cryptography (>=3.4.0)"]
pycrypto = ["pycrypto (>=2.6.0,<2.7.0)"]
pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)"]
test = ["pytest", "pytest-cov"]
[[package]]
name = "python-multipart"
version = "0.0.20"
@ -492,6 +558,32 @@ files = [
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
]
[[package]]
name = "rsa"
version = "4.2"
description = "Pure-Python RSA implementation"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "rsa-4.2.tar.gz", hash = "sha256:aaefa4b84752e3e99bd8333a2e1e3e7a7da64614042bd66f775573424370108a"},
]
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "six"
version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
groups = ["main"]
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
]
[[package]]
name = "sniffio"
version = "1.3.1"
@ -708,4 +800,4 @@ email = ["email-validator"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.11"
content-hash = "21a35caa29ba269d21d8b22f161f5828f99ab497c5d687f013eda16461478541"
content-hash = "00e000faa33bdaefef12840d09b78569135abf3a3a37996816ac4866bce65714"

View File

@ -13,7 +13,9 @@ dependencies = [
"fastapi (>=0.116.1,<0.117.0)",
"uvicorn (>=0.35.0,<0.36.0)",
"python-dotenv (>=1.1.1,<2.0.0)",
"asyncpg (>=0.30.0,<0.31.0)"
"asyncpg (>=0.30.0,<0.31.0)",
"python-jose (>=3.5.0,<4.0.0)",
"itsdangerous (>=2.2.0,<3.0.0)"
]

Binary file not shown.

View File

@ -1,10 +1,11 @@
from sqladmin import ModelView
from src.admin.base_admin import BaseAdmin
from src.database.engines import accounts_engine
from src.database.streamers import Account
class AccountAdmin(ModelView, model=Account):
class AccountAdmin(BaseAdmin, model=Account):
engine = accounts_engine
name = "Account"

View File

@ -1,45 +1,115 @@
from fastapi import Request
from jose import JWTError
import os
from datetime import datetime, timedelta
from typing import Optional
from dotenv import load_dotenv
from fastapi import Request, HTTPException
from jose import jwt, JWTError
from sqlalchemy import select
from sqladmin.authentication import AuthenticationBackend
from starlette import status
from starlette.responses import RedirectResponse
from src.api_v1.auth.jwt import create_access_token, parse_jwt_token
from src.api_v1.auth.security import check_password
from src.api_v1.auth import crud as auth_crud
from src.api_v1.users import crud as users_crud
from src.exceptions import NotAuthenticated, PermissionDenied
from src.database import db_helper
from src.settings.base import settings
from src.database.engines import accounts_session_maker
from src.database.streamers import Account
# --- Конфигурация ---
# Лучше хранить все настройки в одном месте
load_dotenv()
# Ключ для подписи JWT-токенов
SECRET_KEY = os.getenv("SECRET_KEY", "your_default_secret_key")
# Алгоритм подписи
ALGORITHM = "HS256"
ADMIN_EMAIL = "tihon414@gmail.com"
# Время жизни токена доступа
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 24 часа
class AdminAuth(AuthenticationBackend):
async def login(self, request: Request) -> bool:
form = await request.form()
username, password = form['username'], form['password']
async with db_helper.get_async_session_not_closed() as session:
user_model = await auth_crud.get_user_by_login(
login=username,
session=session,
def parse_jwt_token(
token: str,
) -> dict:
try:
payload = jwt.decode(
token=token,
key=SECRET_KEY,
algorithms=[ALGORITHM]
)
if payload["email"] != ADMIN_EMAIL:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
if not check_password(
plain_password=password,
hashed_password=user_model.hashed_password, # type:ignore
):
raise NotAuthenticated()
if user_model.role != 'Админ':
raise PermissionDenied()
jwt_token = create_access_token(
user=user_model
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
request.session.update({'token': jwt_token.access_token})
return payload
return True
def create_access_token(user: Account) -> str:
"""
Создает JWT-токен для пользователя.
:param user: Объект пользователя (модель Account).
:return: Строка с JWT-токеном.
"""
expires_delta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.utcnow() + expires_delta
jwt_data = {
"sub": user.login, # "sub" (subject) - стандартное поле для идентификатора пользователя
"email": user.email,
"id": user.id,
"exp": expire, # "exp" (expiration time) - время истечения срока действия токена
}
token = jwt.encode(claims=jwt_data, key=SECRET_KEY, algorithm=ALGORITHM)
return token
# --- Класс аутентификации для SQLAdmin ---
class AdminAuth(AuthenticationBackend):
"""
Кастомный бэкенд аутентификации для SQLAdmin.
Использует JWT-токены, хранящиеся в сессии.
"""
async def login(self, request: Request) -> bool:
"""
Обрабатывает попытку входа администратора.
"""
form = await request.form()
username, password = form.get("username"), form.get("password")
if not username or not password:
return False
if username != ADMIN_EMAIL or password != os.getenv("ADMIN_PASS"):
return False
async with accounts_session_maker() as session:
query = select(Account).where(Account.email == ADMIN_EMAIL)
result = await session.execute(query)
user: Optional[Account] = result.scalar_one_or_none()
if user is None:
return False
token = create_access_token(user=user)
request.session.update({"token": token})
return True
async def logout(self, request: Request) -> bool:
"""
Обрабатывает выход администратора.
"""
request.session.clear()
return True
@ -63,4 +133,5 @@ class AdminAuth(AuthenticationBackend):
return True
authentication_backend = AdminAuth(secret_key=settings.JWT.SECRET)
# Создаем экземпляр бэкенда
authentication_backend = AdminAuth(secret_key=SECRET_KEY)

6
src/admin/base_admin.py Normal file
View File

@ -0,0 +1,6 @@
from sqladmin import ModelView
class BaseAdmin(ModelView):
can_delete = False
page_size = 100

View File

@ -1,19 +1,25 @@
from sqladmin import ModelView
from src.admin.base_admin import BaseAdmin
from src.database.payments import Payment, Deal, Balance, TinkoffWithdraw, TinkoffWithdrawMethod, StreamersCommission
class PaymentAdmin(ModelView, model=Payment):
class PaymentAdmin(BaseAdmin, model=Payment):
column_searchable_list = [
Payment.order_id,
Payment.streamer_id,
Payment.deal_id,
Payment.balance_id,
]
category = "Payments DB"
name = "Payment"
name_plural = "Payments"
icon = "fa-solid fa-money-bill"
column_list = [Payment.order_id, Payment.streamer_id, Payment.amount, Payment.created_at]
column_searchable_list = [Payment.order_id, Payment.streamer_id]
column_default_sort = ("created_at", True)
class DealAdmin(ModelView, model=Deal):
class DealAdmin(BaseAdmin, model=Deal):
category = "Payments DB"
name = "Deal"
name_plural = "Deals"
@ -22,16 +28,18 @@ class DealAdmin(ModelView, model=Deal):
column_searchable_list = [Deal.deal_id, Deal.streamer_id]
class BalanceAdmin(ModelView, model=Balance):
class BalanceAdmin(BaseAdmin, model=Balance):
category = "Payments DB"
name = "Balance Op"
name_plural = "Balance Ops"
icon = "fa-solid fa-scale-balanced"
column_list = [Balance.id, Balance.streamer_id, Balance.operation_type, Balance.balance_diff, Balance.created_at]
column_list = [Balance.balance_total, Balance.balance_diff, Balance.streamer_id, Balance.operation_type, Balance.created_at]
column_default_sort = ("created_at", True)
column_sortable_list = [Balance.streamer_id, Balance.created_at, Balance.operation_type]
column_searchable_list = [Balance.streamer_id, Balance.operation_type]
class TinkoffWithdrawAdmin(ModelView, model=TinkoffWithdraw):
class TinkoffWithdrawAdmin(BaseAdmin, model=TinkoffWithdraw):
category = "Payments DB"
name = "Tinkoff Withdraw"
name_plural = "Tinkoff Withdraws"
@ -42,7 +50,7 @@ class TinkoffWithdrawAdmin(ModelView, model=TinkoffWithdraw):
column_default_sort = ("created_at", True)
class TinkoffWithdrawMethodAdmin(ModelView, model=TinkoffWithdrawMethod):
class TinkoffWithdrawMethodAdmin(BaseAdmin, model=TinkoffWithdrawMethod):
category = "Payments DB"
name = "Withdraw Method"
name_plural = "Withdraw Methods"
@ -51,7 +59,7 @@ class TinkoffWithdrawMethodAdmin(ModelView, model=TinkoffWithdrawMethod):
TinkoffWithdrawMethod.is_main]
class StreamersCommissionAdmin(ModelView, model=StreamersCommission):
class StreamersCommissionAdmin(BaseAdmin, model=StreamersCommission):
category = "Payments DB"
name = "Commission"
name_plural = "Commissions"

View File

@ -1,11 +1,12 @@
from sqladmin import ModelView
from src.admin.base_admin import BaseAdmin
from src.database.engines import widgets_engine
from src.database.widgets import Widget, File, Donat, Target, DonatePage, Moderation, Filter, FilterWord, VoiceSetting, \
Language, VoiceLanguage, StreamerWidgetPage, StreamerOnline
class FileAdmin(ModelView, model=File):
class FileAdmin(BaseAdmin, model=File):
engine = widgets_engine
category = "Widgets DB"
name = "File"
@ -14,7 +15,7 @@ class FileAdmin(ModelView, model=File):
column_list = [File.id, File.file_name, File.file_type, File.streamer_id, File.created_at]
column_searchable_list = [File.file_name]
class WidgetAdmin(ModelView, model=Widget):
class WidgetAdmin(BaseAdmin, model=Widget):
engine = widgets_engine
category = "Widgets DB"
name = "Widget"
@ -23,7 +24,7 @@ class WidgetAdmin(ModelView, model=Widget):
column_list = [Widget.id, Widget.name, Widget.streamer_id, Widget.is_active, Widget.min_amount]
column_searchable_list = [Widget.name]
class DonatAdmin(ModelView, model=Donat):
class DonatAdmin(BaseAdmin, model=Donat):
engine = widgets_engine
category = "Widgets DB"
name = "Donat"
@ -33,7 +34,7 @@ class DonatAdmin(ModelView, model=Donat):
column_searchable_list = [Donat.donat_user, Donat.text]
column_default_sort = ("created_at", True)
class TargetAdmin(ModelView, model=Target):
class TargetAdmin(BaseAdmin, model=Target):
engine = widgets_engine
category = "Widgets DB"
name = "Target"
@ -41,7 +42,7 @@ class TargetAdmin(ModelView, model=Target):
icon = "fa-solid fa-bullseye"
column_list = [Target.id, Target.text, Target.amount, Target.collected, Target.streamer_id]
class DonatePageAdmin(ModelView, model=DonatePage):
class DonatePageAdmin(BaseAdmin, model=DonatePage):
engine = widgets_engine
category = "Widgets DB"
name = "Donate Page"
@ -50,7 +51,7 @@ class DonatePageAdmin(ModelView, model=DonatePage):
column_list = [DonatePage.id, DonatePage.streamer_login, DonatePage.streamer_id]
column_searchable_list = [DonatePage.streamer_login]
class ModerationAdmin(ModelView, model=Moderation):
class ModerationAdmin(BaseAdmin, model=Moderation):
engine = widgets_engine
category = "Widgets DB"
name = "Moderation"
@ -58,7 +59,7 @@ class ModerationAdmin(ModelView, model=Moderation):
icon = "fa-solid fa-user-shield"
column_list = [c.name for c in Moderation.__table__.c]
class FilterAdmin(ModelView, model=Filter):
class FilterAdmin(BaseAdmin, model=Filter):
engine = widgets_engine
category = "Widgets DB"
name = "Filter"
@ -66,7 +67,7 @@ class FilterAdmin(ModelView, model=Filter):
icon = "fa-solid fa-filter"
column_list = [c.name for c in Filter.__table__.c]
class FilterWordAdmin(ModelView, model=FilterWord):
class FilterWordAdmin(BaseAdmin, model=FilterWord):
engine = widgets_engine
category = "Widgets DB"
name = "Filter Word"
@ -75,7 +76,7 @@ class FilterWordAdmin(ModelView, model=FilterWord):
column_list = [FilterWord.id, FilterWord.word, FilterWord.donat_filter_id]
column_searchable_list = [FilterWord.word]
class VoiceSettingAdmin(ModelView, model=VoiceSetting):
class VoiceSettingAdmin(BaseAdmin, model=VoiceSetting):
engine = widgets_engine
category = "Widgets DB"
name = "Voice Setting"
@ -83,7 +84,7 @@ class VoiceSettingAdmin(ModelView, model=VoiceSetting):
icon = "fa-solid fa-microphone-lines"
column_list = [VoiceSetting.id, VoiceSetting.streamer_id, VoiceSetting.enable, VoiceSetting.min_price]
class LanguageAdmin(ModelView, model=Language):
class LanguageAdmin(BaseAdmin, model=Language):
engine = widgets_engine
category = "Widgets DB"
name = "Language"
@ -92,7 +93,7 @@ class LanguageAdmin(ModelView, model=Language):
column_list = [Language.id, Language.ru_name, Language.en_name, Language.iso_code]
column_searchable_list = [Language.ru_name, Language.en_name, Language.iso_code]
class VoiceLanguageAdmin(ModelView, model=VoiceLanguage):
class VoiceLanguageAdmin(BaseAdmin, model=VoiceLanguage):
engine = widgets_engine
category = "Widgets DB"
name = "Voice Language"
@ -100,7 +101,7 @@ class VoiceLanguageAdmin(ModelView, model=VoiceLanguage):
icon = "fa-solid fa-link"
column_list = [c.name for c in VoiceLanguage.__table__.c]
class StreamerWidgetPageAdmin(ModelView, model=StreamerWidgetPage):
class StreamerWidgetPageAdmin(BaseAdmin, model=StreamerWidgetPage):
engine = widgets_engine
category = "Widgets DB"
name = "Streamer Widget Page"
@ -108,7 +109,7 @@ class StreamerWidgetPageAdmin(ModelView, model=StreamerWidgetPage):
icon = "fa-solid fa-desktop"
column_list = [c.name for c in StreamerWidgetPage.__table__.c]
class StreamerOnlineAdmin(ModelView, model=StreamerOnline):
class StreamerOnlineAdmin(BaseAdmin, model=StreamerOnline):
engine = widgets_engine
category = "Widgets DB"
name = "Streamer Online"

View File

@ -2,7 +2,7 @@ import os
from dotenv import load_dotenv
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
env = load_dotenv()
@ -15,6 +15,11 @@ WIDGETS_DB_URL = os.getenv("WIDGETS_DB_URL")
ACCOUNTS_DB_URL = os.getenv("ACCOUNTS_DB_URL")
payments_engine = create_async_engine(PAYMENTS_DB_URL)
widgets_engine = create_async_engine(WIDGETS_DB_URL)
accounts_engine = create_async_engine(ACCOUNTS_DB_URL)
accounts_engine = create_async_engine(ACCOUNTS_DB_URL)
accounts_session_maker = async_sessionmaker(
bind=accounts_engine,
)

View File

@ -2,6 +2,7 @@ from fastapi import FastAPI
from sqladmin import Admin
from src.admin.account import AccountAdmin
from src.admin.auth import authentication_backend
from src.admin.multi_engine import AdminSessionMaker
from src.admin.payment import DealAdmin, BalanceAdmin, PaymentAdmin, TinkoffWithdrawMethodAdmin, TinkoffWithdrawAdmin, \
StreamersCommissionAdmin
@ -14,6 +15,7 @@ app = FastAPI()
admin = Admin(
app=app,
session_maker=AdminSessionMaker,
authentication_backend=authentication_backend
)
admin.add_view(AccountAdmin)