diff --git a/pyramid_openapi3/__init__.py b/pyramid_openapi3/__init__.py index aaeb3de..367b529 100644 --- a/pyramid_openapi3/__init__.py +++ b/pyramid_openapi3/__init__.py @@ -33,6 +33,7 @@ from urllib.parse import urlparse import hupper +import json import logging import typing as t @@ -147,6 +148,11 @@ def add_explorer_view( route_name: str = "pyramid_openapi3.explorer", template: str = "static/index.html", ui_version: str = "5.12.0", + ui_config: t.Optional[dict[str, t.Any]] = None, + oauth_config: t.Optional[dict[str, t.Any]] = None, + oauth_redirect_route: t.Optional[str] = None, + oauth_redirect_route_name: str = "pyramid_openapi3.explorer.oauth2-redirect", + oauth_redirect_html: str = "static/oauth2-redirect.html", permission: str = NO_PERMISSION_REQUIRED, apiname: str = "pyramid_openapi3", ) -> None: @@ -156,11 +162,28 @@ def add_explorer_view( :param route_name: Route name that's being added :param template: Dotted path to the html template that renders Swagger UI response :param ui_version: Swagger UI version string + :param ui_config: + A dictionary conforming to the SwaggerUI API. + Any settings defined here will override those defined by default. + :param oauth_config: + If defined, then SwaggerUI.initOAuth will be invoked with the supplied config. + :param oauth_redirect_route: + URL path where the redirect will be served. By default the path is constructed + by appending a ``/oauth2-redirect`` path component to the ``route`` parameter. + :param oauth_redirect_route_name: + Route name for the redirect route. + :param oauth_redirect_html: + Dotted path to the html that renders the oauth2-redirect HTML. :param permission: Permission for the explorer view """ + if oauth_redirect_route is None: + oauth_redirect_route = route.rstrip("/") + "/oauth2-redirect" + def register() -> None: - resolved_template = AssetResolver().resolve(template) + asset_resolver = AssetResolver() + resolved_template = asset_resolver.resolve(template) + redirect_html = asset_resolver.resolve(oauth_redirect_html) def explorer_view(request: Request) -> Response: settings = config.registry.settings @@ -171,9 +194,20 @@ def explorer_view(request: Request) -> Response: ) with open(resolved_template.abspath()) as f: template = Template(f.read()) + merged_ui_config = { + "url": request.route_path(settings[apiname]["spec_route_name"]), + "dom_id": "#swagger-ui", + "deepLinking": True, + "validatorUrl": None, + "layout": "StandaloneLayout", + "oauth2RedirectUrl": request.route_url(oauth_redirect_route_name), + } + if ui_config: + merged_ui_config.update(ui_config) html = template.safe_substitute( ui_version=ui_version, - spec_url=request.route_path(settings[apiname]["spec_route_name"]), + ui_config=json.dumps(merged_ui_config), + oauth_config=json.dumps(oauth_config), ) return Response(html) @@ -182,6 +216,16 @@ def explorer_view(request: Request) -> Response: route_name=route_name, permission=permission, view=explorer_view ) + def redirect_view(request: Request) -> FileResponse: + return FileResponse(redirect_html.abspath()) + + config.add_route(oauth_redirect_route_name, oauth_redirect_route) + config.add_view( + route_name=oauth_redirect_route_name, + permission=permission, + view=redirect_view, + ) + config.action((f"{apiname}_add_explorer",), register, order=PHASE0_CONFIG) diff --git a/pyramid_openapi3/static/index.html b/pyramid_openapi3/static/index.html index 19acde9..568e9ad 100644 --- a/pyramid_openapi3/static/index.html +++ b/pyramid_openapi3/static/index.html @@ -69,22 +69,23 @@ diff --git a/pyramid_openapi3/static/oauth2-redirect.html b/pyramid_openapi3/static/oauth2-redirect.html new file mode 100644 index 0000000..5640917 --- /dev/null +++ b/pyramid_openapi3/static/oauth2-redirect.html @@ -0,0 +1,79 @@ + + + + Swagger UI: OAuth2 Redirect + + + + + diff --git a/pyramid_openapi3/tests/test_views.py b/pyramid_openapi3/tests/test_views.py index 2ced200..5d98146 100644 --- a/pyramid_openapi3/tests/test_views.py +++ b/pyramid_openapi3/tests/test_views.py @@ -292,7 +292,7 @@ def test_add_explorer_view() -> None: document.name, route="/foo.yaml", route_name="foo_api_spec" ) - config.pyramid_openapi3_add_explorer() + config.pyramid_openapi3_add_explorer(ui_config={"deepLinking": False}) request = config.registry.queryUtility( IRouteRequest, name="pyramid_openapi3.explorer" ) @@ -301,6 +301,56 @@ def test_add_explorer_view() -> None: ) response = view(request=DummyRequest(config=config), context=None) assert b"Swagger UI" in response.body + assert b"const oauthConfig = null;" in response.body + assert b'"deepLinking": false' in response.body + assert ( + b'"oauth2RedirectUrl": "http://example.com/docs/oauth2-redirect"' + in response.body + ) + + +def test_add_explorer_oauth_view() -> None: + """Test registration of a view serving the Swagger UI.""" + with testConfig() as config: + config.include("pyramid_openapi3") + + with tempfile.NamedTemporaryFile() as document: + document.write(MINIMAL_DOCUMENT) + document.seek(0) + + config.pyramid_openapi3_spec( + document.name, route="/foo.yaml", route_name="foo_api_spec" + ) + + config.pyramid_openapi3_add_explorer( + oauth_redirect_route="/dummy/oauth2-redirect", + oauth_config={ + "usePkceWithAuthorizationCodeGrant": True, + }, + ) + request = config.registry.queryUtility( + IRouteRequest, name="pyramid_openapi3.explorer" + ) + view = config.registry.adapters.registered( + (IViewClassifier, request, Interface), IView, name="" + ) + response = view(request=DummyRequest(config=config), context=None) + assert b"Swagger UI" in response.body + assert b"ui.initOAuth(" in response.body + assert b"usePkceWithAuthorizationCodeGrant" in response.body + assert ( + b'"oauth2RedirectUrl": "http://example.com/dummy/oauth2-redirect"' + in response.body + ) + + request = config.registry.queryUtility( + IRouteRequest, name="pyramid_openapi3.explorer.oauth2-redirect" + ) + view = config.registry.adapters.registered( + (IViewClassifier, request, Interface), IView, name="" + ) + response = view(request=DummyRequest(config=config), context=None) + assert b"Swagger UI: OAuth2 Redirect" in response.body def test_add_multiple_explorer_views() -> None: @@ -344,7 +394,7 @@ def test_add_multiple_explorer_views() -> None: ) response = view(request=DummyRequest(config=config), context=None) assert b"Swagger UI" in response.body - assert b'url: "/foo/openapi.yaml"' in response.body + assert b'"url": "/foo/openapi.yaml"' in response.body request = config.registry.queryUtility(IRouteRequest, name="bar_api_explorer") view = config.registry.adapters.registered( @@ -352,7 +402,7 @@ def test_add_multiple_explorer_views() -> None: ) response = view(request=DummyRequest(config=config), context=None) assert b"Swagger UI" in response.body - assert b'url: "/bar/openapi.yaml"' in response.body + assert b'"url": "/bar/openapi.yaml"' in response.body def test_add_multiple_explorer_views_using_directory() -> None: @@ -398,7 +448,7 @@ def test_add_multiple_explorer_views_using_directory() -> None: ) response = view(request=DummyRequest(config=config), context=None) assert b"Swagger UI" in response.body - assert b'url: "/foo.yaml"' in response.body + assert b'"url": "/foo.yaml"' in response.body request = config.registry.queryUtility(IRouteRequest, name="bar_api_explorer") view = config.registry.adapters.registered( @@ -406,7 +456,7 @@ def test_add_multiple_explorer_views_using_directory() -> None: ) response = view(request=DummyRequest(config=config), context=None) assert b"Swagger UI" in response.body - assert b'url: "/bar.yaml"' in response.body + assert b'"url": "/bar.yaml"' in response.body def test_explorer_view_missing_spec() -> None: