from __future__ import annotations import secrets from datetime import datetime, timedelta, timezone from urllib.parse import urlencode import httpx from fastapi import Depends, HTTPException, Request, Response, status from sqlalchemy import select from sqlalchemy.orm import Session from .config import settings from .database import get_db from .models import UserSession def utcnow() -> datetime: return datetime.now(timezone.utc) def create_session_token() -> str: return secrets.token_urlsafe(32) def set_session_cookie(response: Response, session_token: str) -> None: response.set_cookie( settings.session_cookie_name, session_token, httponly=True, secure=settings.session_cookie_secure, samesite="lax", max_age=60 * 60 * 24 * 14, ) def clear_session_cookie(response: Response) -> None: response.delete_cookie(settings.session_cookie_name) def get_current_session( request: Request, db: Session = Depends(get_db), required: bool = False, ) -> UserSession | None: session_token = request.cookies.get(settings.session_cookie_name) if not session_token: if required: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") return None session = db.scalar(select(UserSession).where(UserSession.session_token == session_token)) if session is None and required: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid session") return session def require_session(session: UserSession | None = Depends(get_current_session)) -> UserSession: if session is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") return session def require_admin(session: UserSession = Depends(require_session)) -> UserSession: if not session.is_admin: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") return session def build_teamsnap_authorize_url(state: str) -> str: params = urlencode( { "client_id": settings.teamsnap_client_id, "redirect_uri": settings.teamsnap_redirect_uri, "response_type": "code", "scope": settings.teamsnap_scope, "state": state, } ) return f"{settings.teamsnap_auth_url}?{params}" def _extract_collection_item(payload: dict) -> dict[str, object] | None: items = payload.get("collection", {}).get("items", []) if not items: return None values: dict[str, object] = {} for field in items[0].get("data", []): name = field.get("name") if isinstance(name, str): values[name] = field.get("value") return values async def fetch_teamsnap_user_id(access_token: str) -> str | None: headers = { "Accept": "application/vnd.collection+json", "Authorization": f"Bearer {access_token}", } try: async with httpx.AsyncClient(timeout=15.0) as client: root_response = await client.get(settings.teamsnap_api_root, headers=headers) root_response.raise_for_status() queries = root_response.json().get("collection", {}).get("queries", []) me_href = next((query.get("href") for query in queries if query.get("rel") == "me"), None) if not isinstance(me_href, str) or not me_href: return None me_response = await client.get(me_href, headers=headers) me_response.raise_for_status() except httpx.HTTPError: return None me_item = _extract_collection_item(me_response.json()) if not me_item: return None user_id = me_item.get("id") return str(user_id) if user_id is not None else None async def exchange_code_for_token(code: str) -> dict: async with httpx.AsyncClient(timeout=15.0) as client: response = await client.post( settings.teamsnap_token_url, data={ "grant_type": "authorization_code", "code": code, "redirect_uri": settings.teamsnap_redirect_uri, "client_id": settings.teamsnap_client_id, "client_secret": settings.teamsnap_client_secret, }, headers={"Accept": "application/json"}, ) if response.status_code >= 400: raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="TeamSnap token exchange failed") return response.json() async def refresh_access_token(refresh_token: str) -> dict: async with httpx.AsyncClient(timeout=15.0) as client: response = await client.post( settings.teamsnap_token_url, data={ "grant_type": "refresh_token", "refresh_token": refresh_token, "client_id": settings.teamsnap_client_id, "client_secret": settings.teamsnap_client_secret, }, headers={"Accept": "application/json"}, ) if response.status_code >= 400: raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail="TeamSnap token refresh failed") return response.json() def update_session_tokens(session: UserSession, token_payload: dict) -> None: session.access_token = token_payload.get("access_token") session.refresh_token = token_payload.get("refresh_token", session.refresh_token) expires_in = token_payload.get("expires_in") session.token_expires_at = utcnow() + timedelta(seconds=int(expires_in)) if expires_in else None