Files
duty-teller/duty_teller/api/dependencies.py
Nikolay Tatarinov 28973489a5 Refactor project structure and enhance Docker configuration
- Updated `.dockerignore` to exclude test and development artifacts, optimizing the Docker image size.
- Refactored `main.py` to delegate execution to `duty_teller.run.main()`, simplifying the entry point.
- Introduced a new `duty_teller` package to encapsulate core functionality, improving modularity and organization.
- Enhanced `pyproject.toml` to define a script for running the application, streamlining the execution process.
- Updated README documentation to reflect changes in project structure and usage instructions.
- Improved Alembic environment configuration to utilize the new package structure for database migrations.
2026-02-18 13:03:14 +03:00

117 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""FastAPI dependencies: DB session, auth, date validation."""
import logging
from typing import Annotated, Generator
from fastapi import Header, HTTPException, Query, Request
from sqlalchemy.orm import Session
import duty_teller.config as config
from duty_teller.api.telegram_auth import validate_init_data_with_reason
from duty_teller.db.repository import get_duties
from duty_teller.db.schemas import DutyWithUser
from duty_teller.db.session import session_scope
from duty_teller.utils.dates import validate_date_range
log = logging.getLogger(__name__)
def _validate_duty_dates(from_date: str, to_date: str) -> None:
try:
validate_date_range(from_date, to_date)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) from e
def get_validated_dates(
from_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="from"),
to_date: str = Query(..., description="ISO date YYYY-MM-DD", alias="to"),
) -> tuple[str, str]:
_validate_duty_dates(from_date, to_date)
return (from_date, to_date)
def get_db_session() -> Generator[Session, None, None]:
with session_scope(config.DATABASE_URL) as session:
yield session
def require_miniapp_username(
request: Request,
x_telegram_init_data: Annotated[
str | None, Header(alias="X-Telegram-Init-Data")
] = None,
) -> str:
return get_authenticated_username(request, x_telegram_init_data)
def _auth_error_detail(auth_reason: str) -> str:
if auth_reason == "hash_mismatch":
return (
"Неверная подпись. Убедитесь, что BOT_TOKEN на сервере совпадает с токеном бота, "
"из которого открыт календарь (тот же бот, что в меню)."
)
return "Неверные данные авторизации"
def _is_private_client(client_host: str | None) -> bool:
if not client_host:
return False
if client_host in ("127.0.0.1", "::1"):
return True
parts = client_host.split(".")
if len(parts) == 4:
try:
a, b, c, d = (int(x) for x in parts)
if (a == 10) or (a == 172 and 16 <= b <= 31) or (a == 192 and b == 168):
return True
except (ValueError, IndexError):
pass
return False
def get_authenticated_username(
request: Request, x_telegram_init_data: str | None
) -> str:
init_data = (x_telegram_init_data or "").strip()
if not init_data:
client_host = request.client.host if request.client else None
if _is_private_client(client_host) or config.MINI_APP_SKIP_AUTH:
if config.MINI_APP_SKIP_AUTH:
log.warning("allowing without initData (MINI_APP_SKIP_AUTH is set)")
return ""
log.warning("no X-Telegram-Init-Data header (client=%s)", client_host)
raise HTTPException(status_code=403, detail="Откройте календарь из Telegram")
max_age = config.INIT_DATA_MAX_AGE_SECONDS or None
username, auth_reason = validate_init_data_with_reason(
init_data, config.BOT_TOKEN, max_age_seconds=max_age
)
if username is None:
log.warning("initData validation failed: %s", auth_reason)
raise HTTPException(status_code=403, detail=_auth_error_detail(auth_reason))
if not config.can_access_miniapp(username):
log.warning("username not in allowlist: %s", username)
raise HTTPException(status_code=403, detail="Доступ запрещён")
return username
def fetch_duties_response(
session: Session, from_date: str, to_date: str
) -> list[DutyWithUser]:
rows = get_duties(session, from_date=from_date, to_date=to_date)
return [
DutyWithUser(
id=duty.id,
user_id=duty.user_id,
start_at=duty.start_at,
end_at=duty.end_at,
full_name=full_name,
event_type=(
duty.event_type
if duty.event_type in ("duty", "unavailable", "vacation")
else "duty"
),
)
for duty, full_name in rows
]