From 294c0ebb2d9c675c7db5a1d67d31d6c39882d48e Mon Sep 17 00:00:00 2001 From: harold Date: Thu, 28 Aug 2025 11:34:22 +0500 Subject: [PATCH] add init commit --- .gitignore | 213 +++++++++++++++++- Dockerfile | 37 +++ docker-compose.yml | 18 ++ poetry.lock | 94 +++++++- pyproject.toml | 4 +- src/__pycache__/main.cpython-311.pyc | Bin 2629 -> 2716 bytes src/admin/__pycache__/account.cpython-311.pyc | Bin 1084 -> 1154 bytes src/admin/__pycache__/payment.cpython-311.pyc | Bin 3542 -> 3827 bytes src/admin/__pycache__/widget.cpython-311.pyc | Bin 7798 -> 7868 bytes src/admin/account.py | 3 +- src/admin/auth.py | 135 ++++++++--- src/admin/base_admin.py | 6 + src/admin/payment.py | 24 +- src/admin/widget.py | 27 +-- .../__pycache__/engines.cpython-311.pyc | Bin 766 -> 886 bytes src/database/engines.py | 9 +- src/main.py | 2 + 17 files changed, 513 insertions(+), 59 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/admin/base_admin.py diff --git a/.gitignore b/.gitignore index 2eea525..9a143c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,212 @@ -.env \ No newline at end of file +# 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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5245015 --- /dev/null +++ b/Dockerfile @@ -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 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e733e4c --- /dev/null +++ b/docker-compose.yml @@ -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" \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 36df6b3..af5dd7c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index b399a8c..e120e24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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)" ] diff --git a/src/__pycache__/main.cpython-311.pyc b/src/__pycache__/main.cpython-311.pyc index c2625347c300744232beaec1c513a94d73f8d36b..1faea133b7a5da0982aee8145efa31ad2934fff9 100644 GIT binary patch delta 711 zcmX>qGDlQ>IWI340}%9nSf4SCm4V?ghyw#aJ{Wvfny7BC&d89$kiwS3mCGH)oy!x& zlgk^$o68r)m&+f;pDPe00G4OZnb@i8l`9)1n=2P32NvSUkVOym z7#U#dfO4|9Mq;xv!R?={kHn5dVmBkP*CMg6Be7R9Bh0ys z#QuxKR%a1rLGlnkP7i4_3$vwg1v6-JPj+C@5f@7=Ey+mDE6GewEXmBzi%&{S&Q8rs z*__Ez$|%cxi#f5N;1+LjYH=}0GB+_hwWx><7=V77DwDslnhEk17bWW@rsQVk>46L? zVw`IJcH>3?e cu(NP7ePjRj$pQr#Z?PSu}x~0h#@%eE{#+E_W1nE>9FsE^icX zE?*QMBSQ*93R@0;u0WJPu3*%}UR?*U5PObnu3VHHBLfpdD&sPsF{@!_GDOJ(#h6p& zQ`y&WEMo%705JqaDWobaP@K%jBs^K1QG^-Bo{GfYkHmh7#1>{ksJBI8rz5c^BC&TP zv7aKb`I!;sSR&Zs45@<47=dmDVhCVlfV&Qll@q9Hcw$GVw7OI#hh4BaErG%wYWGlKQBHvF*~)$Pg7~KGMm}t zJT?g#HlTZN2^JS6>m{b-X6ES?Bv$68=9Lt&1H~uLWs?-YC4i7D&rC^AEh*v!$(?1h znasy7&L#!q6iH9kW>4kP1TsM(Q2ccAEOv*@Z`c(Wg+H(|b25Eo01{t7L<8#wj?GG( KWsEH9K!pGe!-66J diff --git a/src/admin/__pycache__/account.cpython-311.pyc b/src/admin/__pycache__/account.cpython-311.pyc index 4e4bfe9830126e43bbb69a01bded3348531e6498..147bcb55ebe32c146de9b6038bd003422994b0b5 100644 GIT binary patch delta 338 zcmdnP(ZtESoR^o20SFvlug^%C$ScVhHc?$OFohw7C5J7SJ&K)?fr-JLA%(StA%!iK zIhB14`!W^=hSfj}0Z|+&9Kj5loG(FKO~zZCzWFJsIboTpjDD`x-t1Cb36-3BLJp;SXBT3 delta 268 zcmZqT+{3}UoR^o20SM&W*JoHx=CgUwm-~5!+oUqK)@`(#?GqO&uWlUjYnf!=RUxF2=;1*M6$}QHM z{PfJcTdb+MiJ3V?oRc+~HgK^6C5u1=`{c(=`HIpoF3?2QT#$)=n(VhYiVJfRQ*tx& zikK(&F-rl>UCAut0Wue?wMYuY0&A54vVL*c0Ckq;q}mlJ0=bMpTwDeuJ}@&fGTva2 QyMT%wOqOSfX94L30MZ#grvLx| diff --git a/src/admin/__pycache__/payment.cpython-311.pyc b/src/admin/__pycache__/payment.cpython-311.pyc index cf456dbc54f2fe662b9b53e611002ed0c0898462..c46eeb56088e4c888c7a7b6ef54ddb1729f8aae8 100644 GIT binary patch literal 3827 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 delta 2123 zcma)+O>7%g5P*06vtF;g{)yK=YdfivG)bM7qzDR;>Ldzjn^sM&Kto$u3a;@kA(9=_ z-Hlomq<{oM6+#e=!~tbTkBa(pO<}vVY;Gmf@VoS(XFdcUgh~TLxW8O2YgSJ` zl@OUDKdxeQ)*DWJ!!|8fYP35W>y9-@K(d{Nvte6g&7wHmfMZ^9to<+{>-q@oauj1U zo4hCTa8Yh~X-~o(`5iAV8CJuiz0)3%>2pr|2+bZt;f9-?R?BQUror)8!ysxYWr{e7 z{qT2q(Q`BfRpl*F16O&nw~yg4eaR`uY{xPiZPTi^I;(5z+vXATNmodZ#36V!@{H%9 z0&iy?hiLS)myQA}(X;(@1UE#hKz}~hH*we%o3;;ErIIxU-$W0JN8zvNa6g?=wXF88 zMuwV%8?M)##`7(+Ubl|ndOP1)nvysIGnoPSNImF9G>Z|fry?iTT8?SW8Es61FIWVY zW9O&1I&<{tQz-5SMH|OaT+KZ2-9)9eW;@MJ`=yzA_&K(RcoPvabb`bKk)EVAiD{1X zeMy0LFw7^Wdsl2EN*1|dfy?-j8~3j23!HO(&EQ+fHCrT2 z635}U#FFQ85f+ly@K$`6Jl5MsM7`$&AKaO~tjM=4zC^nusyOU9cumsbdfy?>@DkkV zyX9Sk7`%}>(A&BcW9#r^s&xM)$Cqi3#5uBkfo;!e^PcTxDl7b{X&AVgZWkX`NO&(j zB%1JLdM_o5lh@rPtI;NjI+pB|7eNgpc$2fcs=?u`1{2xl!^60h9TVH|eReNlB>pFi z_zG>3SjDk@+E;Shr@eyl=KAs8cP+dcUT0lFU&H`>%CbPf+uSU!+YE z9US&7AN^1Ic#iuR?N`hDT-md!)!i}-MyC22ZBh!&QBZM;JF)f&*Of?ErVYw yKLWmue1*8ye9D|7xv;pT?2$U>UAfZk7#l5f#xigz;;^rQUMSvPC|vRfNbg^~|BdAU diff --git a/src/admin/account.py b/src/admin/account.py index 8ea29f9..4faaa0a 100644 --- a/src/admin/account.py +++ b/src/admin/account.py @@ -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" diff --git a/src/admin/auth.py b/src/admin/auth.py index 41f86be..bb0c218 100644 --- a/src/admin/auth.py +++ b/src/admin/auth.py @@ -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) \ No newline at end of file +# Создаем экземпляр бэкенда +authentication_backend = AdminAuth(secret_key=SECRET_KEY) \ No newline at end of file diff --git a/src/admin/base_admin.py b/src/admin/base_admin.py new file mode 100644 index 0000000..5376c32 --- /dev/null +++ b/src/admin/base_admin.py @@ -0,0 +1,6 @@ +from sqladmin import ModelView + + +class BaseAdmin(ModelView): + can_delete = False + page_size = 100 \ No newline at end of file diff --git a/src/admin/payment.py b/src/admin/payment.py index eefa571..f00896a 100644 --- a/src/admin/payment.py +++ b/src/admin/payment.py @@ -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" diff --git a/src/admin/widget.py b/src/admin/widget.py index 00a80bd..a712187 100644 --- a/src/admin/widget.py +++ b/src/admin/widget.py @@ -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" diff --git a/src/database/__pycache__/engines.cpython-311.pyc b/src/database/__pycache__/engines.cpython-311.pyc index 334a93c666f55a0f4c0333b49c3a550e9d83c2a0..81f2a1dddb95b054043fcd1b2d4dbcdb2845df33 100644 GIT binary patch delta 389 zcmeyz`i+fmIWI340}w>MU7x|lG?7n&@yA4UWvyJ+C{{)WCWcg|WkAW*AQfN`#Re2( z$YO`_Q&=WW5Y;#ZL~)`iXPbD;#v3TclFEgqk{2k-n##L|9Zj4sg)x{x zlj9}GKtD~!TiiMMi7D|Z`6a1&WtvQrQ;Ul;^Yd~Ovr~&En=l%4 zGTvfI%FIjA+tw)9kx_98Z*ND;{KMI4jYGnz`=5=%@@&M(a? zDTWyq4>9ctV=R{#P#wr)#pfn_GpUMsU0}e1CO0s-Fmg;j%4DGSKvMgHr1lL?i4V+7 WQcNEifW#LNae+Y$f{H|drU3wu+gdaL delta 305 zcmeyy_K%fsIWI340}y<6Tc5F-aU!1t%Jqy4Obn?^%YYKAL9$>F#R?Q-$YO)> zQ&^TU0_A}i0;1T_