Refactor duty authentication and event type handling

- Introduced a new function `get_authenticated_username` to centralize Mini App authentication logic, improving code readability and maintainability.
- Updated the duty fetching logic to map unknown event types to "duty" for consistent API responses.
- Enhanced the `get_duties` function to include duties starting on the last day of the specified date range.
- Improved session management in the database layer to ensure rollback on exceptions.
- Added tests to validate the new authentication flow and event type handling.
This commit is contained in:
2026-02-18 09:24:51 +03:00
parent 50347038e9
commit 8697b9e30b
8 changed files with 94 additions and 56 deletions

View File

@@ -46,7 +46,11 @@ def _fetch_duties_response(from_date: str, to_date: str) -> list[DutyWithUser]:
start_at=duty.start_at,
end_at=duty.end_at,
full_name=full_name,
event_type=duty.event_type,
event_type=(
duty.event_type
if duty.event_type in ("duty", "unavailable", "vacation")
else "duty"
),
)
for duty, full_name in rows
]
@@ -87,6 +91,33 @@ def _is_private_client(client_host: str | None) -> bool:
return False
def get_authenticated_username(
request: Request,
x_telegram_init_data: str | None,
) -> str:
"""Validate Mini App auth. Returns username (or "" when bypass allowed); raises HTTPException 403 otherwise."""
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
app = FastAPI(title="Duty Teller API")
app.add_middleware(
CORSMiddleware,
@@ -110,54 +141,10 @@ def list_duties(
request.client.host if request.client else "?",
bool((x_telegram_init_data or "").strip()),
)
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(
"duties: allowing without initData (MINI_APP_SKIP_AUTH is set)"
)
return _fetch_duties_response(from_date, to_date)
log.warning("duties: 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("duties: 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("duties: username not in allowlist")
raise HTTPException(status_code=403, detail="Доступ запрещён")
get_authenticated_username(request, x_telegram_init_data)
return _fetch_duties_response(from_date, to_date)
def _require_same_auth(
request: Request,
x_telegram_init_data: str | None,
) -> None:
"""Raise HTTPException 403 if not allowed (same logic as list_duties)."""
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:
return
log.warning("calendar-events: 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("calendar-events: 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("calendar-events: username not in allowlist")
raise HTTPException(status_code=403, detail="Доступ запрещён")
@app.get("/api/calendar-events", response_model=list[CalendarEvent])
def list_calendar_events(
request: Request,
@@ -166,7 +153,7 @@ def list_calendar_events(
x_telegram_init_data: str | None = Header(None, alias="X-Telegram-Init-Data"),
) -> list[CalendarEvent]:
_validate_duty_dates(from_date, to_date)
_require_same_auth(request, x_telegram_init_data)
get_authenticated_username(request, x_telegram_init_data)
url = config.EXTERNAL_CALENDAR_ICS_URL
if not url:
return []