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:
81
api/app.py
81
api/app.py
@@ -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 []
|
||||
|
||||
Reference in New Issue
Block a user