mirror of
https://github.com/flibusta-apps/services_manager_server.git
synced 2025-12-06 04:25:38 +01:00
Rewrite to rust
This commit is contained in:
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
prisma = "run -p prisma-cli --"
|
||||
35
.github/workflows/codeql-analysis.yml
vendored
35
.github/workflows/codeql-analysis.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '0 12 * * *'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
35
.github/workflows/linters.yaml
vendored
35
.github/workflows/linters.yaml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Linters
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
Run-Pre-Commit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 32
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install pre-commit
|
||||
run: pip3 install pre-commit
|
||||
|
||||
- name: Pre-commit (Push)
|
||||
env:
|
||||
SETUPTOOLS_USE_DISTUTILS: stdlib
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
run: pre-commit run --source ${{ github.event.before }} --origin ${{ github.event.after }} --show-diff-on-failure
|
||||
|
||||
- name: Pre-commit (Pull-Request)
|
||||
env:
|
||||
SETUPTOOLS_USE_DISTUTILS: stdlib
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: pre-commit run --source ${{ github.event.pull_request.base.sha }} --origin ${{ github.event.pull_request.head.sha }} --show-diff-on-failure
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
.vscode
|
||||
/target
|
||||
|
||||
venv
|
||||
|
||||
__pycache__
|
||||
.env
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
exclude: 'docs|node_modules|migrations|.git|.tox'
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.11
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.267'
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: typos-dict-v0.9.26
|
||||
hooks:
|
||||
- id: typos
|
||||
5406
Cargo.lock
generated
Normal file
5406
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
Normal file
27
Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "services_manager_server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"prisma-cli"
|
||||
]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.18.0"
|
||||
|
||||
prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.8", features = ["postgresql"] }
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
|
||||
tokio = { version = "1.28.2", features = ["full"] }
|
||||
axum = { version = "0.6.18", features = ["json"] }
|
||||
axum-prometheus = "0.4.0"
|
||||
chrono = "0.4.26"
|
||||
sentry = "0.31.5"
|
||||
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"]}
|
||||
tower-http = { version = "0.4.3", features = ["trace"] }
|
||||
@@ -1,26 +1,21 @@
|
||||
FROM ghcr.io/flibusta-apps/base_docker_images:3.11-postgres-asyncpg-poetry-buildtime as build-image
|
||||
|
||||
WORKDIR /root/poetry
|
||||
|
||||
COPY pyproject.toml poetry.lock /root/poetry/
|
||||
|
||||
RUN poetry export --without-hashes > requirements.txt \
|
||||
&& . /opt/venv/bin/activate \
|
||||
&& pip install -r requirements.txt --no-cache-dir
|
||||
|
||||
|
||||
FROM ghcr.io/flibusta-apps/base_docker_images:3.11-postgres-runtime as runtime-image
|
||||
FROM rust:bullseye AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./src/ /app/
|
||||
COPY ./scripts/* /root/
|
||||
COPY . .
|
||||
|
||||
ENV VENV_PATH=/opt/venv
|
||||
ENV PATH="$VENV_PATH/bin:$PATH"
|
||||
RUN cargo build --release --bin services_manager_server
|
||||
|
||||
COPY --from=build-image $VENV_PATH $VENV_PATH
|
||||
|
||||
EXPOSE 8080
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
CMD bash /root/start_production.sh
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y openssl ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/target/release/services_manager_server /usr/local/bin
|
||||
ENTRYPOINT ["/usr/local/bin/services_manager_server"]
|
||||
|
||||
1175
poetry.lock
generated
1175
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
3
prisma-cli/.gitignore
vendored
Normal file
3
prisma-cli/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
.env
|
||||
4622
prisma-cli/Cargo.lock
generated
Normal file
4622
prisma-cli/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
prisma-cli/Cargo.toml
Normal file
9
prisma-cli/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "prisma-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", tag = "0.6.8", features = ["postgresql"] }
|
||||
3
prisma-cli/src/main.rs
Normal file
3
prisma-cli/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
prisma_client_rust_cli::run();
|
||||
}
|
||||
21
prisma/schema.prisma
Normal file
21
prisma/schema.prisma
Normal file
@@ -0,0 +1,21 @@
|
||||
generator client {
|
||||
provider = "cargo prisma"
|
||||
output = "../src/prisma.rs"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Service {
|
||||
id Int @id @default(autoincrement())
|
||||
token String @unique @db.VarChar(128)
|
||||
user BigInt
|
||||
status String @db.VarChar(12)
|
||||
created_time DateTime @db.Timestamptz(6)
|
||||
cache String @db.VarChar(12)
|
||||
username String @db.VarChar(64)
|
||||
|
||||
@@map("services")
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
[tool.poetry]
|
||||
name = "services_manager_server"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Kurbanov Bulat <kurbanovbul@gmail.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
fastapi = "^0.95.1"
|
||||
alembic = "^1.10.4"
|
||||
ormar = {extras = ["postgresql"], version = "^0.12.1"}
|
||||
uvicorn = {extras = ["standard"], version = "^0.22.0"}
|
||||
httpx = "^0.24.0"
|
||||
uvloop = "^0.17.0"
|
||||
sentry-sdk = "^1.22.2"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pre-commit = "^2.21.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.black]
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
\.git
|
||||
| \.vscode
|
||||
| \venv
|
||||
| alembic
|
||||
)/
|
||||
'''
|
||||
|
||||
[tool.ruff]
|
||||
fix = true
|
||||
target-version = "py311"
|
||||
src = ["app"]
|
||||
line-length=88
|
||||
ignore = []
|
||||
select = ["B", "C", "E", "F", "W", "B9", "I001"]
|
||||
exclude = [
|
||||
# No need to traverse our git directory
|
||||
".git",
|
||||
# There's no value in checking cache directories
|
||||
"__pycache__",
|
||||
# The conf file is mostly autogenerated, ignore it
|
||||
"src/app/alembic",
|
||||
]
|
||||
|
||||
[tool.ruff.flake8-bugbear]
|
||||
extend-immutable-calls = ["fastapi.File", "fastapi.Form", "fastapi.Security"]
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
max-complexity = 15
|
||||
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["core", "app"]
|
||||
force-sort-within-sections = true
|
||||
force-wrap-aliases = true
|
||||
section-order = ["future", "standard-library", "base_framework", "framework_ext", "third-party", "first-party", "local-folder"]
|
||||
lines-after-imports = 2
|
||||
|
||||
[tool.ruff.isort.sections]
|
||||
base_framework = ["fastapi",]
|
||||
framework_ext = ["starlette"]
|
||||
|
||||
[tool.ruff.pyupgrade]
|
||||
keep-runtime-typing = true
|
||||
@@ -1,11 +0,0 @@
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
response = httpx.get(
|
||||
"http://localhost:8080/healthcheck",
|
||||
headers={"Authorization": os.environ["API_KEY"]},
|
||||
)
|
||||
print(f"HEALTHCHECK STATUS: {response.status_code}")
|
||||
exit(0 if response.status_code == 200 else 1)
|
||||
@@ -1,3 +0,0 @@
|
||||
cd /app
|
||||
alembic -c ./app/alembic.ini upgrade head
|
||||
uvicorn main:app --host 0.0.0.0 --port 8080 --loop uvloop
|
||||
@@ -1,98 +0,0 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = ./app/alembic
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; This defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path.
|
||||
# The path separator used here should be the separator specified by "version_path_separator"
|
||||
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. Valid values are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os # default: use os.pathsep
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
@@ -1 +0,0 @@
|
||||
Generic single-database configuration.
|
||||
@@ -1,67 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy.engine import create_engine
|
||||
|
||||
from core.db import DATABASE_URL
|
||||
|
||||
|
||||
myPath = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, myPath + "/../../")
|
||||
|
||||
config = context.config
|
||||
|
||||
|
||||
from app.models import BaseMeta
|
||||
|
||||
|
||||
target_metadata = BaseMeta.metadata
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""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():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
connectable = create_engine(DATABASE_URL)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection, target_metadata=target_metadata, compare_type=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -1,24 +0,0 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -1,31 +0,0 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 738a796c3f0a
|
||||
Revises: 7a76c257df70
|
||||
Create Date: 2021-12-13 01:34:21.957994
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "738a796c3f0a"
|
||||
down_revision = "7a76c257df70"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"services",
|
||||
sa.Column("privileged", sa.Boolean(), server_default="f", nullable=False),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("services", "privileged")
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,30 +0,0 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 738a796c3f0b
|
||||
Revises: 85ece6cfed22
|
||||
Create Date: 2021-12-13 01:34:21.957994
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "738a796c3f0b"
|
||||
down_revision = "85ece6cfed22"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"services", sa.Column("username", sa.String(length=64), nullable=True)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("services", "username")
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,37 +0,0 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 7a76c257df70
|
||||
Revises:
|
||||
Create Date: 2021-12-04 00:46:17.770026
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "7a76c257df70"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"services",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("token", sa.String(length=128), nullable=False),
|
||||
sa.Column("user", sa.BigInteger(), nullable=False),
|
||||
sa.Column("status", sa.String(length=12), nullable=True),
|
||||
sa.Column("created_time", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("token"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("services")
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,39 +0,0 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 85ece6cfed22
|
||||
Revises: 738a796c3f0a
|
||||
Create Date: 2021-12-27 23:08:26.124204
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "85ece6cfed22"
|
||||
down_revision = "738a796c3f0a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("services", sa.Column("cache", sa.String(length=12), nullable=True))
|
||||
op.drop_column("services", "privileged")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"services",
|
||||
sa.Column(
|
||||
"privileged",
|
||||
sa.BOOLEAN(),
|
||||
server_default=sa.text("false"),
|
||||
autoincrement=False,
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
op.drop_column("services", "cache")
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,11 +0,0 @@
|
||||
from fastapi import HTTPException, Security, status
|
||||
|
||||
from core.auth import default_security
|
||||
from core.config import env_config
|
||||
|
||||
|
||||
async def check_token(api_key: str = Security(default_security)):
|
||||
if api_key != env_config.API_KEY:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Wrong api key!"
|
||||
)
|
||||
@@ -1,40 +0,0 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
import ormar
|
||||
|
||||
from core.db import database, metadata
|
||||
|
||||
|
||||
class BaseMeta(ormar.ModelMeta):
|
||||
metadata = metadata
|
||||
database = database
|
||||
|
||||
|
||||
class Statuses(str, Enum):
|
||||
pending = "pending"
|
||||
approved = "approved"
|
||||
blocked = "blocked"
|
||||
|
||||
|
||||
class CachePrivileges(str, Enum):
|
||||
original = "original"
|
||||
buffer = "buffer"
|
||||
no_cache = "no_cache"
|
||||
|
||||
|
||||
class Service(ormar.Model):
|
||||
class Meta(BaseMeta):
|
||||
tablename = "services"
|
||||
|
||||
id: int = ormar.Integer(primary_key=True) # type: ignore
|
||||
token: str = ormar.String(max_length=128, unique=True) # type: ignore
|
||||
username: str = ormar.String(max_length=64, default="") # type: ignore
|
||||
user: int = ormar.BigInteger() # type: ignore
|
||||
status: str = ormar.String(
|
||||
max_length=12, choices=list(Statuses), default=Statuses.pending
|
||||
) # type: ignore
|
||||
cache: str = ormar.String(
|
||||
max_length=12, choices=list(CachePrivileges), default=CachePrivileges.no_cache
|
||||
) # type: ignore
|
||||
created_time = ormar.DateTime(timezone=True, default=datetime.now)
|
||||
@@ -1,24 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, constr
|
||||
|
||||
from app.models import CachePrivileges, Statuses
|
||||
|
||||
|
||||
class ServiceCreate(BaseModel):
|
||||
token: constr(max_length=128) # type: ignore
|
||||
user: int
|
||||
username: constr(max_length=64) # type: ignore
|
||||
status: Statuses
|
||||
cache: CachePrivileges
|
||||
|
||||
|
||||
class ServiceDetail(BaseModel):
|
||||
id: int
|
||||
token: str
|
||||
username: Optional[str]
|
||||
user: str
|
||||
status: str
|
||||
cache: str
|
||||
created_time: datetime
|
||||
@@ -1,81 +0,0 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from app.depends import check_token
|
||||
from app.models import CachePrivileges, Service, Statuses
|
||||
from app.serializers import ServiceCreate, ServiceDetail
|
||||
|
||||
|
||||
# TODO: add redis cache
|
||||
|
||||
|
||||
router = APIRouter(dependencies=[Depends(check_token)])
|
||||
|
||||
|
||||
@router.get("/", response_model=list[ServiceDetail])
|
||||
async def get_services():
|
||||
return await Service.objects.all()
|
||||
|
||||
|
||||
@router.get("/healthcheck")
|
||||
async def healthcheck():
|
||||
return "Ok!"
|
||||
|
||||
|
||||
@router.get("/{id}/", response_model=ServiceDetail)
|
||||
async def get_service(id: int):
|
||||
service = await Service.objects.get_or_none(id=id)
|
||||
|
||||
if service is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
return service
|
||||
|
||||
|
||||
@router.delete("/{id}/", response_model=ServiceDetail)
|
||||
async def delete_service(id: int):
|
||||
service = await Service.objects.get_or_none(id=id)
|
||||
|
||||
if service is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
await service.delete()
|
||||
|
||||
return service
|
||||
|
||||
|
||||
@router.post("/", response_model=ServiceDetail)
|
||||
async def register_service(data: ServiceCreate):
|
||||
user_services_count = await Service.objects.filter(user=data.user).count()
|
||||
|
||||
if user_services_count >= 3:
|
||||
raise HTTPException(status.HTTP_402_PAYMENT_REQUIRED)
|
||||
|
||||
return await Service.objects.create(**data.dict())
|
||||
|
||||
|
||||
@router.patch("/{id}/update_status", response_model=ServiceDetail)
|
||||
async def update_service_state(id: int, new_status: Statuses):
|
||||
service = await Service.objects.get_or_none(id=id)
|
||||
|
||||
if service is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
service.status = new_status
|
||||
|
||||
await service.update(["status"])
|
||||
|
||||
return service
|
||||
|
||||
|
||||
@router.patch("/{id}/update_cache", response_model=ServiceDetail)
|
||||
async def update_service_cache(id: int, new_cache: CachePrivileges):
|
||||
service = await Service.objects.get_or_none(id=id)
|
||||
|
||||
if service is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
service.cache = new_cache
|
||||
|
||||
await service.update(["cache"])
|
||||
|
||||
return service
|
||||
40
src/config.rs
Normal file
40
src/config.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
pub struct Config {
|
||||
pub api_key: String,
|
||||
|
||||
pub postgres_user: String,
|
||||
pub postgres_password: String,
|
||||
pub postgres_host: String,
|
||||
pub postgres_port: u32,
|
||||
pub postgres_db: String,
|
||||
|
||||
pub sentry_dsn: String
|
||||
}
|
||||
|
||||
|
||||
fn get_env(env: &'static str) -> String {
|
||||
std::env::var(env).unwrap_or_else(|_| panic!("Cannot get the {} env variable", env))
|
||||
}
|
||||
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Config {
|
||||
Config {
|
||||
api_key: get_env("API_KEY"),
|
||||
|
||||
postgres_user: get_env("POSTGRES_USER"),
|
||||
postgres_password: get_env("POSTGRES_PASSWORD"),
|
||||
postgres_host: get_env("POSTGRES_HOST"),
|
||||
postgres_port: get_env("POSTGRES_PORT").parse().unwrap(),
|
||||
postgres_db: get_env("POSTGRES_DB"),
|
||||
|
||||
sentry_dsn: get_env("SENTRY_DSN")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
|
||||
Config::load()
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
import sentry_sdk
|
||||
|
||||
from app.views import router
|
||||
from core.config import env_config
|
||||
from core.db import database
|
||||
|
||||
|
||||
sentry_sdk.init(
|
||||
env_config.SENTRY_DSN,
|
||||
)
|
||||
|
||||
|
||||
def start_app() -> FastAPI:
|
||||
app = FastAPI()
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
app.state.database = database
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup() -> None:
|
||||
database_ = app.state.database
|
||||
if not database_.is_connected:
|
||||
await database_.connect()
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown() -> None:
|
||||
database_ = app.state.database
|
||||
if database_.is_connected:
|
||||
await database_.disconnect()
|
||||
|
||||
return app
|
||||
@@ -1,4 +0,0 @@
|
||||
from fastapi.security import APIKeyHeader
|
||||
|
||||
|
||||
default_security = APIKeyHeader(name="Authorization")
|
||||
@@ -1,16 +0,0 @@
|
||||
from pydantic import BaseSettings
|
||||
|
||||
|
||||
class EnvConfig(BaseSettings):
|
||||
API_KEY: str
|
||||
|
||||
POSTGRES_USER: str
|
||||
POSTGRES_PASSWORD: str
|
||||
POSTGRES_HOST: str
|
||||
POSTGRES_PORT: int
|
||||
POSTGRES_DB: str
|
||||
|
||||
SENTRY_DSN: str
|
||||
|
||||
|
||||
env_config = EnvConfig()
|
||||
@@ -1,15 +0,0 @@
|
||||
from urllib.parse import quote
|
||||
|
||||
from databases import Database
|
||||
from sqlalchemy import MetaData
|
||||
|
||||
from core.config import env_config
|
||||
|
||||
|
||||
DATABASE_URL = (
|
||||
f"postgresql://{env_config.POSTGRES_USER}:{quote(env_config.POSTGRES_PASSWORD)}@"
|
||||
f"{env_config.POSTGRES_HOST}:{env_config.POSTGRES_PORT}/{env_config.POSTGRES_DB}"
|
||||
)
|
||||
|
||||
metadata = MetaData()
|
||||
database = Database(DATABASE_URL, min_size=1, max_size=2)
|
||||
19
src/db.rs
Normal file
19
src/db.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::{prisma::PrismaClient, config::CONFIG};
|
||||
|
||||
|
||||
pub async fn get_prisma_client() -> PrismaClient {
|
||||
let database_url: String = format!(
|
||||
"postgresql://{}:{}@{}:{}/{}?connection_limit=2",
|
||||
CONFIG.postgres_user,
|
||||
CONFIG.postgres_password,
|
||||
CONFIG.postgres_host,
|
||||
CONFIG.postgres_port,
|
||||
CONFIG.postgres_db
|
||||
);
|
||||
|
||||
PrismaClient::_builder()
|
||||
.with_url(database_url)
|
||||
.build()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
from core.app import start_app
|
||||
|
||||
|
||||
app = start_app()
|
||||
33
src/main.rs
Normal file
33
src/main.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod prisma;
|
||||
pub mod views;
|
||||
|
||||
use tracing::info;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
|
||||
async fn start_app() {
|
||||
let app = views::get_router().await;
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
|
||||
|
||||
info!("Start webserver...");
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
info!("Webserver shutdown...")
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(false)
|
||||
.compact()
|
||||
.init();
|
||||
|
||||
let _guard = sentry::init(config::CONFIG.sentry_dsn.clone());
|
||||
|
||||
start_app().await;
|
||||
}
|
||||
1585
src/prisma.rs
Normal file
1585
src/prisma.rs
Normal file
File diff suppressed because one or more lines are too long
198
src/views.rs
Normal file
198
src/views.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use axum::{Router, response::{Response, IntoResponse}, http::{StatusCode, self, Request}, middleware::{Next, self}, Extension, routing::{get, delete, post, patch}, Json, extract::{Path, self}};
|
||||
use axum_prometheus::PrometheusMetricLayer;
|
||||
use serde::Deserialize;
|
||||
use tower_http::trace::{TraceLayer, self};
|
||||
use tracing::Level;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{config::CONFIG, db::get_prisma_client, prisma::{PrismaClient, service}};
|
||||
|
||||
|
||||
pub type Database = Extension<Arc<PrismaClient>>;
|
||||
|
||||
|
||||
//
|
||||
|
||||
async fn get_services(
|
||||
db: Database
|
||||
) -> impl IntoResponse {
|
||||
let services = db.service()
|
||||
.find_many(vec![])
|
||||
.exec()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Json(services).into_response()
|
||||
}
|
||||
|
||||
async fn get_service(
|
||||
Path(id): Path<i32>,
|
||||
db: Database
|
||||
) -> impl IntoResponse {
|
||||
let service = db.service()
|
||||
.find_unique(service::id::equals(id))
|
||||
.exec()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match service {
|
||||
Some(v) => Json(v).into_response(),
|
||||
None => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_service(
|
||||
Path(id): Path<i32>,
|
||||
db: Database
|
||||
) -> impl IntoResponse {
|
||||
let service = db.service()
|
||||
.find_unique(service::id::equals(id))
|
||||
.exec()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
match service {
|
||||
Some(v) => {
|
||||
let _ = db.service()
|
||||
.delete(service::id::equals(id))
|
||||
.exec()
|
||||
.await;
|
||||
|
||||
Json(v).into_response()
|
||||
},
|
||||
None => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateServiceData {
|
||||
#[serde(rename = "token")]
|
||||
pub token: String,
|
||||
#[serde(rename = "user")]
|
||||
pub user: i64,
|
||||
#[serde(rename = "status")]
|
||||
pub status: String,
|
||||
#[serde(rename = "cache")]
|
||||
pub cache: String,
|
||||
#[serde(rename = "username")]
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
async fn create_service(
|
||||
db: Database,
|
||||
extract::Json(data): extract::Json<CreateServiceData>,
|
||||
) -> impl IntoResponse {
|
||||
let CreateServiceData { token, user, status, cache, username } = data;
|
||||
|
||||
let service = db.service()
|
||||
.create(
|
||||
token,
|
||||
user,
|
||||
status,
|
||||
chrono::offset::Local::now().into(),
|
||||
cache,
|
||||
username,
|
||||
vec![]
|
||||
)
|
||||
.exec()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Json(service).into_response()
|
||||
}
|
||||
|
||||
async fn update_state(
|
||||
Path(id): Path<i32>,
|
||||
db: Database,
|
||||
extract::Json(state): extract::Json<String>
|
||||
) -> impl IntoResponse {
|
||||
let service = db.service()
|
||||
.update(
|
||||
service::id::equals(id),
|
||||
vec![
|
||||
service::status::set(state)
|
||||
]
|
||||
)
|
||||
.exec()
|
||||
.await;
|
||||
|
||||
match service {
|
||||
Ok(v) => Json(v).into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_cache(
|
||||
Path(id): Path<i32>,
|
||||
db: Database,
|
||||
extract::Json(cache): extract::Json<String>
|
||||
) -> impl IntoResponse {
|
||||
let service = db.service()
|
||||
.update(
|
||||
service::id::equals(id),
|
||||
vec![
|
||||
service::cache::set(cache)
|
||||
]
|
||||
)
|
||||
.exec()
|
||||
.await;
|
||||
|
||||
match service {
|
||||
Ok(v) => Json(v).into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
|
||||
async fn auth<B>(req: Request<B>, next: Next<B>) -> Result<Response, StatusCode> {
|
||||
let auth_header = req.headers()
|
||||
.get(http::header::AUTHORIZATION)
|
||||
.and_then(|header| header.to_str().ok());
|
||||
|
||||
let auth_header = if let Some(auth_header) = auth_header {
|
||||
auth_header
|
||||
} else {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
};
|
||||
|
||||
if auth_header != CONFIG.api_key {
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
Ok(next.run(req).await)
|
||||
}
|
||||
|
||||
|
||||
pub async fn get_router() -> Router {
|
||||
let client = Arc::new(get_prisma_client().await);
|
||||
|
||||
let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair();
|
||||
|
||||
let app_router = Router::new()
|
||||
.route("/", get(get_services))
|
||||
.route("/:id/", get(get_service))
|
||||
.route("/:id/", delete(delete_service))
|
||||
.route("/", post(create_service))
|
||||
.route("/:id/update_status", patch(update_state))
|
||||
.route("/:id/update_cache", patch(update_cache))
|
||||
|
||||
.layer(middleware::from_fn(auth))
|
||||
.layer(Extension(client))
|
||||
.layer(prometheus_layer);
|
||||
|
||||
let metric_router = Router::new()
|
||||
.route("/metrics", get(|| async move { metric_handle.render() }));
|
||||
|
||||
Router::new()
|
||||
.nest("/", app_router)
|
||||
.nest("/", metric_router)
|
||||
.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(trace::DefaultMakeSpan::new()
|
||||
.level(Level::INFO))
|
||||
.on_response(trace::DefaultOnResponse::new()
|
||||
.level(Level::INFO)),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user