Loading portal/api/agenda.py +1 −1 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ from ..model import Key @bp.get("/agendas/-/items") @require_valid(Key) def the_agenda_items(): def the_agenda_items() -> list[dict[str, str]]: tasks = sorted( TaigaUserStoryProvider.get_the_one().fetch_user_stories(), key=lambda task: task["kanban_order"], Loading portal/api/auth.py +22 −12 Original line number Diff line number Diff line # Login code adapted from the flaskr tutorial (see LICENSE-3RD-PARTY.md for details) # See https://flask.palletsprojects.com/en/2.0.x/tutorial/views/ import functools from typing import Any, Callable from flask import request, g from flask import g, request from portal import db from ..model import Key, User from ..problem_details import ProblemResponse, not_found, unauthorized from .blueprint import bp from ..model import User, Key from ..problem_details import not_found, unauthorized @bp.before_app_request def set_user_if_valid(): if not request.authorization or request.authorization.type != "basic": def set_user_if_valid() -> None: if ( not request.authorization or request.authorization.type != "basic" or request.authorization.username is None or request.authorization.password is None ): g.user = None return Loading @@ -23,15 +29,19 @@ def set_user_if_valid(): @bp.before_app_request def set_key_if_valid(): if not request.authorization or request.authorization.type != "bearer": def set_key_if_valid() -> None: if ( not request.authorization or request.authorization.type != "bearer" or request.authorization.token is None ): g.key = None return g.key = Key.get_by_secret_unless_expired(request.authorization.token) def require_valid(credential_type): def require_valid(credential_type: type[User | Key]) -> Callable: """ Requires that valid credentials are provided before allowing access to a route. If no valid credentials are provided, abort and send a 401. Loading @@ -41,9 +51,9 @@ def require_valid(credential_type): - `@require_valid(Key)` to require a valid key """ def decorator(route): def decorator(route: Callable) -> Callable: @functools.wraps(route) def protected_route(**kwargs): def protected_route(**kwargs: Any) -> Any: # Determine which credentials to use credentials = {User: g.user, Key: g.key}.get(credential_type) Loading @@ -60,14 +70,14 @@ def require_valid(credential_type): @bp.post("/keys") @require_valid(User) def post_key(): def post_key() -> tuple[dict, int]: key, secret = Key.generate(g.user) return key.to_dict() | {"secret": secret}, 201 @bp.delete("/keys/<string:uuid>") @require_valid(Key) def delete_key(uuid: str): def delete_key(uuid: str) -> tuple[str, int] | ProblemResponse: key_to_delete = Key.get_only(uuid) if not key_to_delete: Loading portal/api/rest.py +3 −3 Original line number Diff line number Diff line from flask import render_template from ..problem_details import ProblemResponse, not_found from .blueprint import bp from ..problem_details import not_found @bp.get("/<path:_>") def fallback_not_found(_): def fallback_not_found(_: str) -> ProblemResponse: """ If you can't find a matching API endpoint, fallback to returning a 404. Loading @@ -16,5 +16,5 @@ def fallback_not_found(_): @bp.get("/") def index(): def index() -> str: return render_template("redocly.html") portal/api/version.py +2 −1 Original line number Diff line number Diff line import json from datetime import datetime from pathlib import Path from typing import Any from .blueprint import bp @bp.get("/version") def get_version(): def get_version() -> dict[str, Any]: portal_version_path = Path("./PORTAL_VERSION") if portal_version_path.is_file(): with open("PORTAL_VERSION") as f: Loading portal/main.py +4 −3 Original line number Diff line number Diff line from flask import Blueprint, render_template, redirect, url_for from flask import Blueprint, redirect, render_template, url_for from werkzeug import Response bp = Blueprint("main", __name__) @bp.get("/", defaults={"_": ""}) @bp.get("/<path:_>") def index(_): def index(_: str) -> str: """ Serve the index page at /*, i.e. at any path below / and at / itself. Loading @@ -16,5 +17,5 @@ def index(_): @bp.get("/api/") def redirect_to_api(): def redirect_to_api() -> Response: return redirect(url_for("api.index")) Loading
portal/api/agenda.py +1 −1 Original line number Diff line number Diff line Loading @@ -7,7 +7,7 @@ from ..model import Key @bp.get("/agendas/-/items") @require_valid(Key) def the_agenda_items(): def the_agenda_items() -> list[dict[str, str]]: tasks = sorted( TaigaUserStoryProvider.get_the_one().fetch_user_stories(), key=lambda task: task["kanban_order"], Loading
portal/api/auth.py +22 −12 Original line number Diff line number Diff line # Login code adapted from the flaskr tutorial (see LICENSE-3RD-PARTY.md for details) # See https://flask.palletsprojects.com/en/2.0.x/tutorial/views/ import functools from typing import Any, Callable from flask import request, g from flask import g, request from portal import db from ..model import Key, User from ..problem_details import ProblemResponse, not_found, unauthorized from .blueprint import bp from ..model import User, Key from ..problem_details import not_found, unauthorized @bp.before_app_request def set_user_if_valid(): if not request.authorization or request.authorization.type != "basic": def set_user_if_valid() -> None: if ( not request.authorization or request.authorization.type != "basic" or request.authorization.username is None or request.authorization.password is None ): g.user = None return Loading @@ -23,15 +29,19 @@ def set_user_if_valid(): @bp.before_app_request def set_key_if_valid(): if not request.authorization or request.authorization.type != "bearer": def set_key_if_valid() -> None: if ( not request.authorization or request.authorization.type != "bearer" or request.authorization.token is None ): g.key = None return g.key = Key.get_by_secret_unless_expired(request.authorization.token) def require_valid(credential_type): def require_valid(credential_type: type[User | Key]) -> Callable: """ Requires that valid credentials are provided before allowing access to a route. If no valid credentials are provided, abort and send a 401. Loading @@ -41,9 +51,9 @@ def require_valid(credential_type): - `@require_valid(Key)` to require a valid key """ def decorator(route): def decorator(route: Callable) -> Callable: @functools.wraps(route) def protected_route(**kwargs): def protected_route(**kwargs: Any) -> Any: # Determine which credentials to use credentials = {User: g.user, Key: g.key}.get(credential_type) Loading @@ -60,14 +70,14 @@ def require_valid(credential_type): @bp.post("/keys") @require_valid(User) def post_key(): def post_key() -> tuple[dict, int]: key, secret = Key.generate(g.user) return key.to_dict() | {"secret": secret}, 201 @bp.delete("/keys/<string:uuid>") @require_valid(Key) def delete_key(uuid: str): def delete_key(uuid: str) -> tuple[str, int] | ProblemResponse: key_to_delete = Key.get_only(uuid) if not key_to_delete: Loading
portal/api/rest.py +3 −3 Original line number Diff line number Diff line from flask import render_template from ..problem_details import ProblemResponse, not_found from .blueprint import bp from ..problem_details import not_found @bp.get("/<path:_>") def fallback_not_found(_): def fallback_not_found(_: str) -> ProblemResponse: """ If you can't find a matching API endpoint, fallback to returning a 404. Loading @@ -16,5 +16,5 @@ def fallback_not_found(_): @bp.get("/") def index(): def index() -> str: return render_template("redocly.html")
portal/api/version.py +2 −1 Original line number Diff line number Diff line import json from datetime import datetime from pathlib import Path from typing import Any from .blueprint import bp @bp.get("/version") def get_version(): def get_version() -> dict[str, Any]: portal_version_path = Path("./PORTAL_VERSION") if portal_version_path.is_file(): with open("PORTAL_VERSION") as f: Loading
portal/main.py +4 −3 Original line number Diff line number Diff line from flask import Blueprint, render_template, redirect, url_for from flask import Blueprint, redirect, render_template, url_for from werkzeug import Response bp = Blueprint("main", __name__) @bp.get("/", defaults={"_": ""}) @bp.get("/<path:_>") def index(_): def index(_: str) -> str: """ Serve the index page at /*, i.e. at any path below / and at / itself. Loading @@ -16,5 +17,5 @@ def index(_): @bp.get("/api/") def redirect_to_api(): def redirect_to_api() -> Response: return redirect(url_for("api.index"))