Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0fd8be8
delete cloudflared.deb"
Apr 26, 2026
0fae65e
feat(models): Start implementing the role system, add a ban table, an…
May 21, 2026
4507278
chore(.gitignore): remove tracking of different caches
May 21, 2026
71c6cd3
refactor(tg_link): rename MagicTokens to MagicToken
May 21, 2026
9ce16d4
fix(bot): send login-link URL on start when telegram is not linked
May 21, 2026
6ff4fc1
test(auth): add JWT unit tests
May 21, 2026
0acd843
refactor(models): Improve the readability of field checks in the bans…
May 22, 2026
193c2f2
feat(models): Add validation of the revoked_at field at the database …
May 22, 2026
4f205d0
feat(models): Add checks for the expires_at and revoked_at fields, an…
May 22, 2026
a7126ce
Merge branch 'main' into feature/roles-and-ban-system
Fl1riX May 22, 2026
bd8baac
refactor(models): Rename MagicTokens to MagicToken across the codebas…
May 22, 2026
76ae31b
fix(modedls): Add foreigkey to the appointments table relationship
May 22, 2026
16e8ac7
Fix/backround clear magic tokens (#17)
Fl1riX May 22, 2026
431d08c
feat(ci): Add Docker image building and pushing to the ghcr registry …
Fl1riX May 23, 2026
c73f102
chore: remove old SSH deploy workflow (#19)
Fl1riX May 23, 2026
9b5c47d
Fix/put things in order (#20)
Fl1riX May 23, 2026
4cf54f9
fix(compose): Fix port forwarding to localhost only (#21)
Fl1riX May 25, 2026
da6a42d
fix(compose): Fix the error of accessing the database outside the loc…
Fl1riX May 25, 2026
04375e2
Revert "fix(compose): Fix the error of accessing the database outside…
Fl1riX May 25, 2026
3d5d04e
Fix/magic link bot auth (#24)
Fl1riX May 29, 2026
683fdd7
Feature/docker security improvements (#25)
Fl1riX May 29, 2026
4b151c4
fix: Add && to the docker image build command (#27)
Fl1riX May 30, 2026
b79c134
Feature/heartbit endpoint (#28)
Fl1riX May 30, 2026
8f9b791
feat: Bring the architecture to a clean archeticture. In the structur…
Fl1riX May 30, 2026
971bb34
Fix/alembic migrations (#30)
Fl1riX Jun 1, 2026
69e2a76
fix(migrations): Add alembic.ini file (#31)
Fl1riX Jun 1, 2026
928b38b
refactor: refactor: simplify ban uniqueness constraint and database s…
Fl1riX Jun 2, 2026
a0f32f4
feat(tasks): add background task for revoking expired bans
Fl1riX Jun 2, 2026
9d68e55
Merge branch 'main' into feature/roles-and-ban-system
Fl1riX Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
from src.presentation.middlewares import MetricsMiddleware
from src.infrastructure.tasks.cleanup_magic_tokens import cleanup_telegram_tokens
from src.config import get_database_url
from src.infrastructure.tasks.remove_bans import remove_expired_bans
from src.infrastructure.db import database

scheduler = AsyncIOScheduler() # Планировщик

@asynccontextmanager
async def lifespan(app: FastAPI):
Expand All @@ -24,17 +28,31 @@ async def lifespan(app: FastAPI):
DB_URL
)

SessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
database.SessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False
)

app.state.SessionLocal = SessionLocal
@app.on_event("startup")
async def startup():
scheduler.add_job(
cleanup_telegram_tokens,
"interval",
minutes=5
)
scheduler.add_job(
remove_expired_bans,
"interval",
Comment on lines +37 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Регистрация обработчика события startup внутри контекста lifespan может повлиять на то, когда именно задачи будут запланированы.

Поскольку декоратор теперь выполняется при входе в контекст lifespan, а не при импорте модуля, обработчик может быть зарегистрирован слишком поздно или более одного раза, если контекст будет входиться повторно. Рассмотрите варианты:

  • Оставить обработчик on_event("startup") на этапе импорта модуля и выполнять только привязку SessionLocal в lifespan, или
  • Перенести планирование задач полностью в lifespan (без on_event), чтобы явно контролировать момент добавления задач и запуска планировщика.

Также убедитесь с помощью тестов или логов, что логика startup выполняется ровно один раз в нужный момент жизненного цикла приложения.

Original comment in English

issue (bug_risk): Registering the startup event handler inside the lifespan context may affect when the jobs are actually scheduled.

Because the decorator now runs when the lifespan context is entered rather than at import time, the handler may be registered too late or more than once if the context is re‑entered. Consider either:

  • Keeping the on_event("startup") handler at module import time and only wiring SessionLocal in lifespan, or
  • Moving the job scheduling entirely into lifespan (without on_event) so you explicitly control when jobs are added and the scheduler is started.

Also verify via tests or logs that the startup logic runs exactly once at the intended point in the app lifecycle.

minutes=10
)
scheduler.start()

# При старте выполняется ко до yiled
yield
# выполняется код после yiled и остановка

scheduler.dispose()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Использование dispose() для AsyncIOScheduler, скорее всего, некорректно; shutdown() — это ожидаемый API.

AsyncIOScheduler в APScheduler 3.x должен останавливаться через scheduler.shutdown(...), а не dispose(). dispose() — это метод SQLAlchemy engine и, скорее всего, приведёт здесь к AttributeError. Замените его на scheduler.shutdown() (при необходимости настроив параметр wait).

Original comment in English

issue (bug_risk): Using dispose() on AsyncIOScheduler is likely incorrect; shutdown() is the intended API.

AsyncIOScheduler in APScheduler 3.x should be stopped with scheduler.shutdown(...), not dispose(). dispose() is a SQLAlchemy engine method and will likely trigger an AttributeError here. Replace this with scheduler.shutdown() (optionally configuring wait).

await engine.dispose()


Expand Down Expand Up @@ -69,18 +87,6 @@ async def lifespan(app: FastAPI):
allow_credentials=True
)

scheduler = AsyncIOScheduler() # Планировщик
@app.on_event("startup")
async def startup():
scheduler.add_job(
cleanup_telegram_tokens,
"interval",
minutes=5
)
scheduler.start()



@app.get("/")
@limiter.limit("5/minute")
def welcome(request: Request):
Expand Down
3 changes: 1 addition & 2 deletions src/domain/models/ban_model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
from typing import TYPE_CHECKING

from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Integer, String, func
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates

from src.infrastructure.db.database import Base
Expand Down Expand Up @@ -56,7 +56,6 @@ class Ban(Base):
unique=True,
postgresql_where=(
revoked_at.is_(None)
& (expires_at.is_(None) | (expires_at > func.now()))
),
),
)
Expand Down
17 changes: 10 additions & 7 deletions src/infrastructure/db/database.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from sqlalchemy.orm import declarative_base
from fastapi import Request
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker
)

Base = declarative_base()

async def get_db(request: Request):

async with request.app.state.SessionLocal() as session:
try:
SessionLocal: async_sessionmaker[AsyncSession] | None = None

async def get_db():
if SessionLocal is None:
raise RuntimeError("Database is not initialied")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (typo): Опечатка в сообщении RuntimeError.

Замените initialied на initialized в тексте ошибки для более понятных логов.

Original comment in English

nitpick (typo): Typo in the RuntimeError message.

Change initialied to initialized in the error message for clearer logs.

async with SessionLocal() as session:
yield session
finally:
await session.close()



2 changes: 1 addition & 1 deletion src/infrastructure/tasks/cleanup_magic_tokens.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from sqlalchemy import delete, or_
from datetime import datetime, timezone

from src.infrastructure.db.database import SessionLocal
from src.domain.models import MagicToken
from src.logger import logger
from src.infrastructure.db.database import SessionLocal

async def cleanup_telegram_tokens():
"""Очистка истекших токенов"""
Expand Down
19 changes: 19 additions & 0 deletions src/infrastructure/tasks/remove_bans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlalchemy import update
from datetime import datetime, timezone

from src.domain.models.ban_model import Ban
from src.infrastructure.db.database import SessionLocal

async def remove_expired_bans():
async with SessionLocal() as db:
await db.execute(update(Ban).where(
Ban.expires_at.is_not(None),
Ban.expires_at < datetime.now(timezone.utc),
Ban.revoked_at.is_(None)
).values(
revoked_at = datetime.now(timezone.utc),
revoked_reason="Expiration of the Term"
)
Comment on lines +7 to +16

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Дважды использовать datetime.now(timezone.utc) в одном и том же update может привести к слегка несовпадающим временным меткам.

Здесь datetime.now(timezone.utc) вычисляется отдельно для условия where и для revoked_at. Под нагрузкой они могут немного отличаться, что усложняет анализ данных. Вычислите now = datetime.now(timezone.utc) один раз (например, в начале функции) и используйте его в обоих местах, чтобы сравнение и сохраняемая временная метка были согласованы.

Original comment in English

suggestion: Using datetime.now(timezone.utc) twice inside the same update may lead to slightly inconsistent timestamps.

Here, datetime.now(timezone.utc) is evaluated separately for the where clause and revoked_at. Under load, these can differ slightly, making data harder to reason about. Compute now = datetime.now(timezone.utc) once (e.g., at the start of the function) and reuse it in both places to keep the comparison and stored timestamp aligned.

)

await db.commit()
Loading