From 87ef037421204e673ba9c84566b5da17c770db2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Mon, 4 Dec 2023 13:46:32 +0100 Subject: [PATCH] more api docs and tweaks for auth and updates API --- src/c3nav/api/api.py | 19 ++++-- src/c3nav/editor/api/schemas.py | 10 ++- src/c3nav/mapdata/api/updates.py | 84 ++++++++++++++++++++------ src/c3nav/site/static/site/js/c3nav.js | 2 +- 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/src/c3nav/api/api.py b/src/c3nav/api/api.py index 1ce7a7d1..4666f18d 100644 --- a/src/c3nav/api/api.py +++ b/src/c3nav/api/api.py @@ -15,12 +15,16 @@ class AuthStatusSchema(Schema): """ key_type: APIKeyType = APIField( title="api key type", + description="the type of api KEY THAT IS BEING USED" ) readonly: bool = APIField( title="read only", description="if true, no API operations that modify data can be called" ) - scopes: list[str] + scopes: list[str] = APIField( + title="authorized scopes", + description="scopes available with the current authorization", + ) @auth_api_router.get('/status/', summary="get status", @@ -40,14 +44,19 @@ def get_status(request): class APITokenSchema(Schema): - """ - An API token to be used with Bearer authentication - """ - token: NonEmptyStr + token: NonEmptyStr = APIField( + title="API token", + description="API token to be directly used with `Authorization: Bearer ` HTTP header." + ) @auth_api_router.get('/session/', response=APITokenSchema, auth=None, summary="get session-bound token") def session_token(request): + """ + Get an API token that is bound to the transmitted session cookie. + + Keep in mind that this API token will be invalid if the session gets signed out or similar. + """ session_id = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None) return {"token": "anonymous" if session_id is None else f"session:{session_id}"} diff --git a/src/c3nav/editor/api/schemas.py b/src/c3nav/editor/api/schemas.py index d41932a4..26c8292b 100644 --- a/src/c3nav/editor/api/schemas.py +++ b/src/c3nav/editor/api/schemas.py @@ -50,10 +50,16 @@ EditorGeometriesCacheReferenceElem = Annotated[ class EditorGeometriesPropertiesSchema(Schema): id: EditorID type: NonEmptyStr - space: Optional[EditorID] = None + space: Union[ + Annotated[EditorID, APIField(title="level")], + Annotated[None, APIField(title="null")] + ] = APIField(None, title="lolala") level: Optional[EditorID] = None bounds: bool = False - color: Optional[str] = None + color: Union[ + Annotated[str, APIField(title="color")], + Annotated[None, APIField(title="no color")] + ] = None opacity: Optional[float] = None # todo: range diff --git a/src/c3nav/mapdata/api/updates.py b/src/c3nav/mapdata/api/updates.py index 565cc355..ecf04ccc 100644 --- a/src/c3nav/mapdata/api/updates.py +++ b/src/c3nav/mapdata/api/updates.py @@ -1,16 +1,14 @@ -from typing import Optional +from typing import Optional, Union, Annotated from urllib.parse import urlparse from django.http import HttpResponse -from ninja import Router as APIRouter +from ninja import Router as APIRouter, Field as APIField from ninja import Schema from pydantic import PositiveInt from c3nav.api.auth import auth_responses from c3nav.api.utils import NonEmptyStr -from c3nav.mapdata.api.base import api_etag from c3nav.mapdata.models import MapUpdate -from c3nav.mapdata.schemas.responses import BoundsSchema from c3nav.mapdata.utils.cache.stats import increment_cache_key from c3nav.mapdata.utils.user import get_user_data from c3nav.mapdata.views import set_tile_access_cookie @@ -19,28 +17,78 @@ updates_api_router = APIRouter(tags=["updates"]) class UserDataSchema(Schema): - logged_in: bool - allow_editor: bool - allow_control_panel: bool - has_positions: bool - title: NonEmptyStr - subtitle: NonEmptyStr - permissions: list[PositiveInt] + logged_in: bool = APIField( + title="logged in", + description="whether a user is logged in", + ) + allow_editor: bool = APIField( + title="editor access", + description="whether the user signed in can access the editor (or accessing the editor is possible as guest)." + "this does not mean that the current API authorization allows accessing the editor API.", + ) + allow_control_panel: bool = APIField( + title="control panel access", + description="whether the user signed in can access the control panel.", + ) + has_positions: bool = APIField( + title="user has positions", + description="whether the user signed in has created any positions", + ) + title: NonEmptyStr = APIField( + title="user data title", + description="data to show in the top right corner. can be the user name or `Login` or similar", + example="ada_lovelace", + ) + subtitle: NonEmptyStr = APIField( + title="user data subtitle", + description="a description of the current user data state to display below the user data title", + example="3 areas unlocked", + ) + permissions: list[PositiveInt] = APIField( + title="access permissions", + description="IDs of access restrictions that this user (even if maybe not signed in) has access to", + example=[2, 5], + ) class FetchUpdatesResponseSchema(Schema): - last_site_update: PositiveInt - last_map_update: NonEmptyStr - user: Optional[UserDataSchema] = None + last_site_update: PositiveInt = APIField( + title="ID of the last site update", + description="If this ID changes, it means a major code change may have occured. " + "A reload of all data is recommended.", + example=1, + ) + last_map_update: NonEmptyStr = APIField( + title="string identifier of the last map update", + description="Map updates are incremental, not every map update will change all data. API endpoitns will be " + "aware of this. Use `E-Tag` and `If-None-Match` on API endpoints to query if the data has changed.", + ) + user_data: Union[ + Annotated[UserDataSchema, APIField( + title="user data", + description="always supplied, unless it is a cross-origin request", + )], + Annotated[None, APIField( + title="null", + description="only for cross-origin requests", + )], + ] = APIField(None, + title="user data", + description="user data of this request. ommited for cross-origin requests.", + ) @updates_api_router.get('/fetch/', summary="fetch updates", - description="get regular updates.\n\n" - "this endpoint also sets/updates the tile access cookie." - "if not called regularly, the tileserver will ignore your access permissions.\n\n" - "this endpoint can be called cross-origin, but it will have no user data then.", response={200: FetchUpdatesResponseSchema, **auth_responses}) def fetch_updates(request, response: HttpResponse): + """ + Get regular updates. + + This endpoint also sets/updates the tile access cookie. + If not called regularly, the tileserver will ignore your access permissions. + + This endpoint can be called cross-origin, but it will have no user data then. + """ cross_origin = request.META.get('HTTP_ORIGIN') if cross_origin is not None: try: diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 107f2820..200ad61e 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -1689,7 +1689,7 @@ c3nav = { c3nav.last_site_update = data.last_site_update; c3nav._maybe_load_site_update(c3nav.state); } - c3nav._set_user_data(data.user); + c3nav._set_user_data(data.user_data); }, _maybe_load_site_update: function(state) { if (c3nav.new_site_update && !state.modal && (!state.routing || !state.origin || !state.destination)) {