more api docs and tweaks for auth and updates API

This commit is contained in:
Laura Klünder 2023-12-04 13:46:32 +01:00
parent 64088759f5
commit 87ef037421
4 changed files with 89 additions and 26 deletions

View file

@ -15,12 +15,16 @@ class AuthStatusSchema(Schema):
""" """
key_type: APIKeyType = APIField( key_type: APIKeyType = APIField(
title="api key type", title="api key type",
description="the type of api KEY THAT IS BEING USED"
) )
readonly: bool = APIField( readonly: bool = APIField(
title="read only", title="read only",
description="if true, no API operations that modify data can be called" 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", @auth_api_router.get('/status/', summary="get status",
@ -40,14 +44,19 @@ def get_status(request):
class APITokenSchema(Schema): class APITokenSchema(Schema):
""" token: NonEmptyStr = APIField(
An API token to be used with Bearer authentication title="API token",
""" description="API token to be directly used with `Authorization: Bearer <token>` HTTP header."
token: NonEmptyStr )
@auth_api_router.get('/session/', response=APITokenSchema, auth=None, @auth_api_router.get('/session/', response=APITokenSchema, auth=None,
summary="get session-bound token") summary="get session-bound token")
def session_token(request): 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) session_id = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
return {"token": "anonymous" if session_id is None else f"session:{session_id}"} return {"token": "anonymous" if session_id is None else f"session:{session_id}"}

View file

@ -50,10 +50,16 @@ EditorGeometriesCacheReferenceElem = Annotated[
class EditorGeometriesPropertiesSchema(Schema): class EditorGeometriesPropertiesSchema(Schema):
id: EditorID id: EditorID
type: NonEmptyStr 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 level: Optional[EditorID] = None
bounds: bool = False 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 opacity: Optional[float] = None # todo: range

View file

@ -1,16 +1,14 @@
from typing import Optional from typing import Optional, Union, Annotated
from urllib.parse import urlparse from urllib.parse import urlparse
from django.http import HttpResponse 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 ninja import Schema
from pydantic import PositiveInt from pydantic import PositiveInt
from c3nav.api.auth import auth_responses from c3nav.api.auth import auth_responses
from c3nav.api.utils import NonEmptyStr from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.api.base import api_etag
from c3nav.mapdata.models import MapUpdate 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.cache.stats import increment_cache_key
from c3nav.mapdata.utils.user import get_user_data from c3nav.mapdata.utils.user import get_user_data
from c3nav.mapdata.views import set_tile_access_cookie from c3nav.mapdata.views import set_tile_access_cookie
@ -19,28 +17,78 @@ updates_api_router = APIRouter(tags=["updates"])
class UserDataSchema(Schema): class UserDataSchema(Schema):
logged_in: bool logged_in: bool = APIField(
allow_editor: bool title="logged in",
allow_control_panel: bool description="whether a user is logged in",
has_positions: bool )
title: NonEmptyStr allow_editor: bool = APIField(
subtitle: NonEmptyStr title="editor access",
permissions: list[PositiveInt] 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): class FetchUpdatesResponseSchema(Schema):
last_site_update: PositiveInt last_site_update: PositiveInt = APIField(
last_map_update: NonEmptyStr title="ID of the last site update",
user: Optional[UserDataSchema] = None 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", @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}) response={200: FetchUpdatesResponseSchema, **auth_responses})
def fetch_updates(request, response: HttpResponse): 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') cross_origin = request.META.get('HTTP_ORIGIN')
if cross_origin is not None: if cross_origin is not None:
try: try:

View file

@ -1689,7 +1689,7 @@ c3nav = {
c3nav.last_site_update = data.last_site_update; c3nav.last_site_update = data.last_site_update;
c3nav._maybe_load_site_update(c3nav.state); 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) { _maybe_load_site_update: function(state) {
if (c3nav.new_site_update && !state.modal && (!state.routing || !state.origin || !state.destination)) { if (c3nav.new_site_update && !state.modal && (!state.routing || !state.origin || !state.destination)) {