add init commit
This commit is contained in:
parent
330d39373f
commit
294c0ebb2d
213
.gitignore
vendored
213
.gitignore
vendored
@ -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
37
Dockerfile
Normal 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
18
docker-compose.yml
Normal 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
94
poetry.lock
generated
@ -124,6 +124,25 @@ files = [
|
|||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{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]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.116.1"
|
version = "0.116.1"
|
||||||
@ -242,6 +261,18 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
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]]
|
[[package]]
|
||||||
name = "jinja2"
|
name = "jinja2"
|
||||||
version = "3.1.6"
|
version = "3.1.6"
|
||||||
@ -331,6 +362,18 @@ files = [
|
|||||||
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
{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]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.11.7"
|
version = "2.11.7"
|
||||||
@ -480,6 +523,29 @@ files = [
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
cli = ["click (>=5.0)"]
|
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]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.20"
|
version = "0.0.20"
|
||||||
@ -492,6 +558,32 @@ files = [
|
|||||||
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
|
{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]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
@ -708,4 +800,4 @@ email = ["email-validator"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.11"
|
python-versions = ">=3.11"
|
||||||
content-hash = "21a35caa29ba269d21d8b22f161f5828f99ab497c5d687f013eda16461478541"
|
content-hash = "00e000faa33bdaefef12840d09b78569135abf3a3a37996816ac4866bce65714"
|
||||||
|
@ -13,7 +13,9 @@ dependencies = [
|
|||||||
"fastapi (>=0.116.1,<0.117.0)",
|
"fastapi (>=0.116.1,<0.117.0)",
|
||||||
"uvicorn (>=0.35.0,<0.36.0)",
|
"uvicorn (>=0.35.0,<0.36.0)",
|
||||||
"python-dotenv (>=1.1.1,<2.0.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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,10 +1,11 @@
|
|||||||
from sqladmin import ModelView
|
from sqladmin import ModelView
|
||||||
|
|
||||||
|
from src.admin.base_admin import BaseAdmin
|
||||||
from src.database.engines import accounts_engine
|
from src.database.engines import accounts_engine
|
||||||
from src.database.streamers import Account
|
from src.database.streamers import Account
|
||||||
|
|
||||||
|
|
||||||
class AccountAdmin(ModelView, model=Account):
|
class AccountAdmin(BaseAdmin, model=Account):
|
||||||
engine = accounts_engine
|
engine = accounts_engine
|
||||||
|
|
||||||
name = "Account"
|
name = "Account"
|
||||||
|
@ -1,45 +1,115 @@
|
|||||||
from fastapi import Request
|
import os
|
||||||
from jose import JWTError
|
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 sqladmin.authentication import AuthenticationBackend
|
||||||
|
from starlette import status
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
|
|
||||||
from src.api_v1.auth.jwt import create_access_token, parse_jwt_token
|
from src.database.engines import accounts_session_maker
|
||||||
from src.api_v1.auth.security import check_password
|
from src.database.streamers import Account
|
||||||
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
|
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):
|
def parse_jwt_token(
|
||||||
async def login(self, request: Request) -> bool:
|
token: str,
|
||||||
form = await request.form()
|
) -> dict:
|
||||||
username, password = form['username'], form['password']
|
try:
|
||||||
async with db_helper.get_async_session_not_closed() as session:
|
payload = jwt.decode(
|
||||||
user_model = await auth_crud.get_user_by_login(
|
token=token,
|
||||||
login=username,
|
key=SECRET_KEY,
|
||||||
session=session,
|
algorithms=[ALGORITHM]
|
||||||
|
)
|
||||||
|
if payload["email"] != ADMIN_EMAIL:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
)
|
)
|
||||||
|
except JWTError:
|
||||||
if not check_password(
|
raise HTTPException(
|
||||||
plain_password=password,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
hashed_password=user_model.hashed_password, # type:ignore
|
detail="Could not validate credentials",
|
||||||
):
|
|
||||||
raise NotAuthenticated()
|
|
||||||
|
|
||||||
if user_model.role != 'Админ':
|
|
||||||
raise PermissionDenied()
|
|
||||||
|
|
||||||
jwt_token = create_access_token(
|
|
||||||
user=user_model
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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:
|
async def logout(self, request: Request) -> bool:
|
||||||
|
"""
|
||||||
|
Обрабатывает выход администратора.
|
||||||
|
"""
|
||||||
request.session.clear()
|
request.session.clear()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -63,4 +133,5 @@ class AdminAuth(AuthenticationBackend):
|
|||||||
return True
|
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
6
src/admin/base_admin.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from sqladmin import ModelView
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAdmin(ModelView):
|
||||||
|
can_delete = False
|
||||||
|
page_size = 100
|
@ -1,19 +1,25 @@
|
|||||||
from sqladmin import ModelView
|
from sqladmin import ModelView
|
||||||
|
|
||||||
|
from src.admin.base_admin import BaseAdmin
|
||||||
from src.database.payments import Payment, Deal, Balance, TinkoffWithdraw, TinkoffWithdrawMethod, StreamersCommission
|
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"
|
category = "Payments DB"
|
||||||
name = "Payment"
|
name = "Payment"
|
||||||
name_plural = "Payments"
|
name_plural = "Payments"
|
||||||
icon = "fa-solid fa-money-bill"
|
icon = "fa-solid fa-money-bill"
|
||||||
column_list = [Payment.order_id, Payment.streamer_id, Payment.amount, Payment.created_at]
|
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)
|
column_default_sort = ("created_at", True)
|
||||||
|
|
||||||
|
|
||||||
class DealAdmin(ModelView, model=Deal):
|
class DealAdmin(BaseAdmin, model=Deal):
|
||||||
category = "Payments DB"
|
category = "Payments DB"
|
||||||
name = "Deal"
|
name = "Deal"
|
||||||
name_plural = "Deals"
|
name_plural = "Deals"
|
||||||
@ -22,16 +28,18 @@ class DealAdmin(ModelView, model=Deal):
|
|||||||
column_searchable_list = [Deal.deal_id, Deal.streamer_id]
|
column_searchable_list = [Deal.deal_id, Deal.streamer_id]
|
||||||
|
|
||||||
|
|
||||||
class BalanceAdmin(ModelView, model=Balance):
|
class BalanceAdmin(BaseAdmin, model=Balance):
|
||||||
category = "Payments DB"
|
category = "Payments DB"
|
||||||
name = "Balance Op"
|
name = "Balance Op"
|
||||||
name_plural = "Balance Ops"
|
name_plural = "Balance Ops"
|
||||||
icon = "fa-solid fa-scale-balanced"
|
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_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"
|
category = "Payments DB"
|
||||||
name = "Tinkoff Withdraw"
|
name = "Tinkoff Withdraw"
|
||||||
name_plural = "Tinkoff Withdraws"
|
name_plural = "Tinkoff Withdraws"
|
||||||
@ -42,7 +50,7 @@ class TinkoffWithdrawAdmin(ModelView, model=TinkoffWithdraw):
|
|||||||
column_default_sort = ("created_at", True)
|
column_default_sort = ("created_at", True)
|
||||||
|
|
||||||
|
|
||||||
class TinkoffWithdrawMethodAdmin(ModelView, model=TinkoffWithdrawMethod):
|
class TinkoffWithdrawMethodAdmin(BaseAdmin, model=TinkoffWithdrawMethod):
|
||||||
category = "Payments DB"
|
category = "Payments DB"
|
||||||
name = "Withdraw Method"
|
name = "Withdraw Method"
|
||||||
name_plural = "Withdraw Methods"
|
name_plural = "Withdraw Methods"
|
||||||
@ -51,7 +59,7 @@ class TinkoffWithdrawMethodAdmin(ModelView, model=TinkoffWithdrawMethod):
|
|||||||
TinkoffWithdrawMethod.is_main]
|
TinkoffWithdrawMethod.is_main]
|
||||||
|
|
||||||
|
|
||||||
class StreamersCommissionAdmin(ModelView, model=StreamersCommission):
|
class StreamersCommissionAdmin(BaseAdmin, model=StreamersCommission):
|
||||||
category = "Payments DB"
|
category = "Payments DB"
|
||||||
name = "Commission"
|
name = "Commission"
|
||||||
name_plural = "Commissions"
|
name_plural = "Commissions"
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from sqladmin import ModelView
|
from sqladmin import ModelView
|
||||||
|
|
||||||
|
from src.admin.base_admin import BaseAdmin
|
||||||
from src.database.engines import widgets_engine
|
from src.database.engines import widgets_engine
|
||||||
from src.database.widgets import Widget, File, Donat, Target, DonatePage, Moderation, Filter, FilterWord, VoiceSetting, \
|
from src.database.widgets import Widget, File, Donat, Target, DonatePage, Moderation, Filter, FilterWord, VoiceSetting, \
|
||||||
Language, VoiceLanguage, StreamerWidgetPage, StreamerOnline
|
Language, VoiceLanguage, StreamerWidgetPage, StreamerOnline
|
||||||
|
|
||||||
|
|
||||||
class FileAdmin(ModelView, model=File):
|
class FileAdmin(BaseAdmin, model=File):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "File"
|
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_list = [File.id, File.file_name, File.file_type, File.streamer_id, File.created_at]
|
||||||
column_searchable_list = [File.file_name]
|
column_searchable_list = [File.file_name]
|
||||||
|
|
||||||
class WidgetAdmin(ModelView, model=Widget):
|
class WidgetAdmin(BaseAdmin, model=Widget):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Widget"
|
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_list = [Widget.id, Widget.name, Widget.streamer_id, Widget.is_active, Widget.min_amount]
|
||||||
column_searchable_list = [Widget.name]
|
column_searchable_list = [Widget.name]
|
||||||
|
|
||||||
class DonatAdmin(ModelView, model=Donat):
|
class DonatAdmin(BaseAdmin, model=Donat):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Donat"
|
name = "Donat"
|
||||||
@ -33,7 +34,7 @@ class DonatAdmin(ModelView, model=Donat):
|
|||||||
column_searchable_list = [Donat.donat_user, Donat.text]
|
column_searchable_list = [Donat.donat_user, Donat.text]
|
||||||
column_default_sort = ("created_at", True)
|
column_default_sort = ("created_at", True)
|
||||||
|
|
||||||
class TargetAdmin(ModelView, model=Target):
|
class TargetAdmin(BaseAdmin, model=Target):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Target"
|
name = "Target"
|
||||||
@ -41,7 +42,7 @@ class TargetAdmin(ModelView, model=Target):
|
|||||||
icon = "fa-solid fa-bullseye"
|
icon = "fa-solid fa-bullseye"
|
||||||
column_list = [Target.id, Target.text, Target.amount, Target.collected, Target.streamer_id]
|
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
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Donate Page"
|
name = "Donate Page"
|
||||||
@ -50,7 +51,7 @@ class DonatePageAdmin(ModelView, model=DonatePage):
|
|||||||
column_list = [DonatePage.id, DonatePage.streamer_login, DonatePage.streamer_id]
|
column_list = [DonatePage.id, DonatePage.streamer_login, DonatePage.streamer_id]
|
||||||
column_searchable_list = [DonatePage.streamer_login]
|
column_searchable_list = [DonatePage.streamer_login]
|
||||||
|
|
||||||
class ModerationAdmin(ModelView, model=Moderation):
|
class ModerationAdmin(BaseAdmin, model=Moderation):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Moderation"
|
name = "Moderation"
|
||||||
@ -58,7 +59,7 @@ class ModerationAdmin(ModelView, model=Moderation):
|
|||||||
icon = "fa-solid fa-user-shield"
|
icon = "fa-solid fa-user-shield"
|
||||||
column_list = [c.name for c in Moderation.__table__.c]
|
column_list = [c.name for c in Moderation.__table__.c]
|
||||||
|
|
||||||
class FilterAdmin(ModelView, model=Filter):
|
class FilterAdmin(BaseAdmin, model=Filter):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Filter"
|
name = "Filter"
|
||||||
@ -66,7 +67,7 @@ class FilterAdmin(ModelView, model=Filter):
|
|||||||
icon = "fa-solid fa-filter"
|
icon = "fa-solid fa-filter"
|
||||||
column_list = [c.name for c in Filter.__table__.c]
|
column_list = [c.name for c in Filter.__table__.c]
|
||||||
|
|
||||||
class FilterWordAdmin(ModelView, model=FilterWord):
|
class FilterWordAdmin(BaseAdmin, model=FilterWord):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Filter Word"
|
name = "Filter Word"
|
||||||
@ -75,7 +76,7 @@ class FilterWordAdmin(ModelView, model=FilterWord):
|
|||||||
column_list = [FilterWord.id, FilterWord.word, FilterWord.donat_filter_id]
|
column_list = [FilterWord.id, FilterWord.word, FilterWord.donat_filter_id]
|
||||||
column_searchable_list = [FilterWord.word]
|
column_searchable_list = [FilterWord.word]
|
||||||
|
|
||||||
class VoiceSettingAdmin(ModelView, model=VoiceSetting):
|
class VoiceSettingAdmin(BaseAdmin, model=VoiceSetting):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Voice Setting"
|
name = "Voice Setting"
|
||||||
@ -83,7 +84,7 @@ class VoiceSettingAdmin(ModelView, model=VoiceSetting):
|
|||||||
icon = "fa-solid fa-microphone-lines"
|
icon = "fa-solid fa-microphone-lines"
|
||||||
column_list = [VoiceSetting.id, VoiceSetting.streamer_id, VoiceSetting.enable, VoiceSetting.min_price]
|
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
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Language"
|
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_list = [Language.id, Language.ru_name, Language.en_name, Language.iso_code]
|
||||||
column_searchable_list = [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
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Voice Language"
|
name = "Voice Language"
|
||||||
@ -100,7 +101,7 @@ class VoiceLanguageAdmin(ModelView, model=VoiceLanguage):
|
|||||||
icon = "fa-solid fa-link"
|
icon = "fa-solid fa-link"
|
||||||
column_list = [c.name for c in VoiceLanguage.__table__.c]
|
column_list = [c.name for c in VoiceLanguage.__table__.c]
|
||||||
|
|
||||||
class StreamerWidgetPageAdmin(ModelView, model=StreamerWidgetPage):
|
class StreamerWidgetPageAdmin(BaseAdmin, model=StreamerWidgetPage):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Streamer Widget Page"
|
name = "Streamer Widget Page"
|
||||||
@ -108,7 +109,7 @@ class StreamerWidgetPageAdmin(ModelView, model=StreamerWidgetPage):
|
|||||||
icon = "fa-solid fa-desktop"
|
icon = "fa-solid fa-desktop"
|
||||||
column_list = [c.name for c in StreamerWidgetPage.__table__.c]
|
column_list = [c.name for c in StreamerWidgetPage.__table__.c]
|
||||||
|
|
||||||
class StreamerOnlineAdmin(ModelView, model=StreamerOnline):
|
class StreamerOnlineAdmin(BaseAdmin, model=StreamerOnline):
|
||||||
engine = widgets_engine
|
engine = widgets_engine
|
||||||
category = "Widgets DB"
|
category = "Widgets DB"
|
||||||
name = "Streamer Online"
|
name = "Streamer Online"
|
||||||
|
Binary file not shown.
@ -2,7 +2,7 @@ import os
|
|||||||
|
|
||||||
from dotenv import load_dotenv
|
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()
|
env = load_dotenv()
|
||||||
|
|
||||||
@ -15,6 +15,11 @@ WIDGETS_DB_URL = os.getenv("WIDGETS_DB_URL")
|
|||||||
ACCOUNTS_DB_URL = os.getenv("ACCOUNTS_DB_URL")
|
ACCOUNTS_DB_URL = os.getenv("ACCOUNTS_DB_URL")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
payments_engine = create_async_engine(PAYMENTS_DB_URL)
|
payments_engine = create_async_engine(PAYMENTS_DB_URL)
|
||||||
widgets_engine = create_async_engine(WIDGETS_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,
|
||||||
|
)
|
@ -2,6 +2,7 @@ from fastapi import FastAPI
|
|||||||
from sqladmin import Admin
|
from sqladmin import Admin
|
||||||
|
|
||||||
from src.admin.account import AccountAdmin
|
from src.admin.account import AccountAdmin
|
||||||
|
from src.admin.auth import authentication_backend
|
||||||
from src.admin.multi_engine import AdminSessionMaker
|
from src.admin.multi_engine import AdminSessionMaker
|
||||||
from src.admin.payment import DealAdmin, BalanceAdmin, PaymentAdmin, TinkoffWithdrawMethodAdmin, TinkoffWithdrawAdmin, \
|
from src.admin.payment import DealAdmin, BalanceAdmin, PaymentAdmin, TinkoffWithdrawMethodAdmin, TinkoffWithdrawAdmin, \
|
||||||
StreamersCommissionAdmin
|
StreamersCommissionAdmin
|
||||||
@ -14,6 +15,7 @@ app = FastAPI()
|
|||||||
admin = Admin(
|
admin = Admin(
|
||||||
app=app,
|
app=app,
|
||||||
session_maker=AdminSessionMaker,
|
session_maker=AdminSessionMaker,
|
||||||
|
authentication_backend=authentication_backend
|
||||||
)
|
)
|
||||||
admin.add_view(AccountAdmin)
|
admin.add_view(AccountAdmin)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user