You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
Host a Swagger UI both in the static documentation and by the server
This commit is contained in:
@ -14,13 +14,23 @@
|
|||||||
|
|
||||||
use aide::{
|
use aide::{
|
||||||
axum::ApiRouter,
|
axum::ApiRouter,
|
||||||
openapi::{OAuth2Flow, OAuth2Flows, OpenApi, SecurityScheme, Server, ServerVariable},
|
openapi::{OAuth2Flow, OAuth2Flows, OpenApi, SecurityScheme, Server},
|
||||||
|
};
|
||||||
|
use axum::{
|
||||||
|
extract::{FromRef, FromRequestParts, State},
|
||||||
|
http::HeaderName,
|
||||||
|
response::Html,
|
||||||
|
Json, Router,
|
||||||
};
|
};
|
||||||
use axum::{extract::FromRequestParts, Json, Router};
|
|
||||||
use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
|
use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
use mas_axum_utils::FancyError;
|
||||||
use mas_http::CorsLayerExt;
|
use mas_http::CorsLayerExt;
|
||||||
use mas_router::{OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, SimpleRoute};
|
use mas_router::{
|
||||||
|
ApiDoc, ApiDocCallback, OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, Route, SimpleRoute,
|
||||||
|
UrlBuilder,
|
||||||
|
};
|
||||||
|
use mas_templates::{ApiDocContext, Templates};
|
||||||
use tower_http::cors::{Any, CorsLayer};
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
|
|
||||||
mod call_context;
|
mod call_context;
|
||||||
@ -35,6 +45,8 @@ pub fn router<S>() -> (OpenApi, Router<S>)
|
|||||||
where
|
where
|
||||||
S: Clone + Send + Sync + 'static,
|
S: Clone + Send + Sync + 'static,
|
||||||
CallContext: FromRequestParts<S>,
|
CallContext: FromRequestParts<S>,
|
||||||
|
Templates: FromRef<S>,
|
||||||
|
UrlBuilder: FromRef<S>,
|
||||||
{
|
{
|
||||||
let mut api = OpenApi::default();
|
let mut api = OpenApi::default();
|
||||||
let router = ApiRouter::<S>::new()
|
let router = ApiRouter::<S>::new()
|
||||||
@ -70,17 +82,6 @@ where
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.security_requirement_scopes("oauth2", ["urn:mas:admin"])
|
.security_requirement_scopes("oauth2", ["urn:mas:admin"])
|
||||||
.server(Server {
|
|
||||||
url: "{base}".to_owned(),
|
|
||||||
variables: IndexMap::from([(
|
|
||||||
"base".to_owned(),
|
|
||||||
ServerVariable {
|
|
||||||
default: "/".to_owned(),
|
|
||||||
..ServerVariable::default()
|
|
||||||
},
|
|
||||||
)]),
|
|
||||||
..Server::default()
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let router = router
|
let router = router
|
||||||
@ -88,16 +89,55 @@ where
|
|||||||
.route(
|
.route(
|
||||||
"/api/spec.json",
|
"/api/spec.json",
|
||||||
axum::routing::get({
|
axum::routing::get({
|
||||||
let res = Json(api.clone());
|
let api = api.clone();
|
||||||
move || std::future::ready(res.clone())
|
move |State(url_builder): State<UrlBuilder>| {
|
||||||
|
// Let's set the servers to the HTTP base URL
|
||||||
|
let mut api = api.clone();
|
||||||
|
api.servers = vec![Server {
|
||||||
|
url: url_builder.http_base().to_string(),
|
||||||
|
..Server::default()
|
||||||
|
}];
|
||||||
|
|
||||||
|
std::future::ready(Json(api))
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
// Serve the Swagger API reference
|
||||||
|
.route(ApiDoc::route(), axum::routing::get(swagger))
|
||||||
|
.route(
|
||||||
|
ApiDocCallback::route(),
|
||||||
|
axum::routing::get(swagger_callback),
|
||||||
|
)
|
||||||
.layer(
|
.layer(
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
.allow_origin(Any)
|
.allow_origin(Any)
|
||||||
.allow_methods(Any)
|
.allow_methods(Any)
|
||||||
.allow_otel_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]),
|
.allow_otel_headers([
|
||||||
|
AUTHORIZATION,
|
||||||
|
ACCEPT,
|
||||||
|
CONTENT_TYPE,
|
||||||
|
// Swagger will send this header, so we have to allow it to avoid CORS errors
|
||||||
|
HeaderName::from_static("x-requested-with"),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
(api, router)
|
(api, router)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn swagger(
|
||||||
|
State(url_builder): State<UrlBuilder>,
|
||||||
|
State(templates): State<Templates>,
|
||||||
|
) -> Result<Html<String>, FancyError> {
|
||||||
|
let ctx = ApiDocContext::from_url_builder(&url_builder);
|
||||||
|
let res = templates.render_swagger(&ctx)?;
|
||||||
|
Ok(Html(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn swagger_callback(
|
||||||
|
State(url_builder): State<UrlBuilder>,
|
||||||
|
State(templates): State<Templates>,
|
||||||
|
) -> Result<Html<String>, FancyError> {
|
||||||
|
let ctx = ApiDocContext::from_url_builder(&url_builder);
|
||||||
|
let res = templates.render_swagger_callback(&ctx)?;
|
||||||
|
Ok(Html(res))
|
||||||
|
}
|
||||||
|
@ -23,6 +23,9 @@
|
|||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
use aide::openapi::{Server, ServerVariable};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
/// This is a dummy state, it should never be used.
|
/// This is a dummy state, it should never be used.
|
||||||
///
|
///
|
||||||
/// We use it to generate the API schema, which doesn't execute any request.
|
/// We use it to generate the API schema, which doesn't execute any request.
|
||||||
@ -58,10 +61,25 @@ macro_rules! impl_from_ref {
|
|||||||
impl_from_request_parts!(mas_storage::BoxRepository);
|
impl_from_request_parts!(mas_storage::BoxRepository);
|
||||||
impl_from_request_parts!(mas_storage::BoxClock);
|
impl_from_request_parts!(mas_storage::BoxClock);
|
||||||
impl_from_request_parts!(mas_handlers::BoundActivityTracker);
|
impl_from_request_parts!(mas_handlers::BoundActivityTracker);
|
||||||
impl_from_ref!(mas_keystore::Keystore);
|
impl_from_ref!(mas_router::UrlBuilder);
|
||||||
|
impl_from_ref!(mas_templates::Templates);
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let (api, _) = mas_handlers::admin_api_router::<DummyState>();
|
let (mut api, _) = mas_handlers::admin_api_router::<DummyState>();
|
||||||
|
|
||||||
|
// Set the server list to a configurable base URL
|
||||||
|
api.servers = vec![Server {
|
||||||
|
url: "{base}".to_owned(),
|
||||||
|
variables: IndexMap::from([(
|
||||||
|
"base".to_owned(),
|
||||||
|
ServerVariable {
|
||||||
|
default: "/".to_owned(),
|
||||||
|
..ServerVariable::default()
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
..Server::default()
|
||||||
|
}];
|
||||||
|
|
||||||
let mut stdout = std::io::stdout();
|
let mut stdout = std::io::stdout();
|
||||||
serde_json::to_writer_pretty(&mut stdout, &api)?;
|
serde_json::to_writer_pretty(&mut stdout, &api)?;
|
||||||
|
|
||||||
|
@ -870,3 +870,24 @@ pub struct GraphQLPlayground;
|
|||||||
impl SimpleRoute for GraphQLPlayground {
|
impl SimpleRoute for GraphQLPlayground {
|
||||||
const PATH: &'static str = "/graphql/playground";
|
const PATH: &'static str = "/graphql/playground";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `GET /api/spec.json`
|
||||||
|
pub struct ApiSpec;
|
||||||
|
|
||||||
|
impl SimpleRoute for ApiSpec {
|
||||||
|
const PATH: &'static str = "/api/spec.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `GET /api/doc/`
|
||||||
|
pub struct ApiDoc;
|
||||||
|
|
||||||
|
impl SimpleRoute for ApiDoc {
|
||||||
|
const PATH: &'static str = "/api/doc/";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `GET /api/doc/oauth2-callback`
|
||||||
|
pub struct ApiDocCallback;
|
||||||
|
|
||||||
|
impl SimpleRoute for ApiDocCallback {
|
||||||
|
const PATH: &'static str = "/api/doc/oauth2-callback";
|
||||||
|
}
|
||||||
|
@ -28,7 +28,9 @@ pub struct UrlBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl UrlBuilder {
|
impl UrlBuilder {
|
||||||
fn absolute_url_for<U>(&self, destination: &U) -> Url
|
/// Create an absolute URL for a route
|
||||||
|
#[must_use]
|
||||||
|
pub fn absolute_url_for<U>(&self, destination: &U) -> Url
|
||||||
where
|
where
|
||||||
U: Route,
|
U: Route,
|
||||||
{
|
{
|
||||||
|
@ -337,6 +337,35 @@ impl TemplateContext for AppContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Context used by the `swagger/doc.html` template
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct ApiDocContext {
|
||||||
|
openapi_url: Url,
|
||||||
|
callback_url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiDocContext {
|
||||||
|
/// Constructs a context for the API documentation page giben the
|
||||||
|
/// [`UrlBuilder`]
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
|
||||||
|
Self {
|
||||||
|
openapi_url: url_builder.absolute_url_for(&mas_router::ApiSpec),
|
||||||
|
callback_url: url_builder.absolute_url_for(&mas_router::ApiDocCallback),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateContext for ApiDocContext {
|
||||||
|
fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
|
||||||
|
vec![Self::from_url_builder(&url_builder)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Fields of the login form
|
/// Fields of the login form
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
|
@ -42,16 +42,16 @@ mod macros;
|
|||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
context::{
|
context::{
|
||||||
AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext, DeviceLinkContext,
|
ApiDocContext, AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext,
|
||||||
DeviceLinkFormField, EmailAddContext, EmailRecoveryContext, EmailVerificationContext,
|
DeviceLinkContext, DeviceLinkFormField, EmailAddContext, EmailRecoveryContext,
|
||||||
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
EmailVerificationContext, EmailVerificationPageContext, EmptyContext, ErrorContext,
|
||||||
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
|
FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
|
||||||
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryExpiredContext,
|
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
|
||||||
RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext,
|
ReauthFormField, RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
|
||||||
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
|
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
|
||||||
SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext, UpstreamExistingLinkContext,
|
RegisterFormField, SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext,
|
||||||
UpstreamRegister, UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf,
|
UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
|
||||||
WithLanguage, WithOptionalSession, WithSession,
|
UpstreamSuggestLink, WithCaptcha, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
|
||||||
},
|
},
|
||||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||||
};
|
};
|
||||||
@ -324,6 +324,12 @@ register_templates! {
|
|||||||
/// Render the frontend app
|
/// Render the frontend app
|
||||||
pub fn render_app(WithLanguage<AppContext>) { "app.html" }
|
pub fn render_app(WithLanguage<AppContext>) { "app.html" }
|
||||||
|
|
||||||
|
/// Render the Swagger API reference
|
||||||
|
pub fn render_swagger(ApiDocContext) { "swagger/doc.html" }
|
||||||
|
|
||||||
|
/// Render the Swagger OAuth2 callback page
|
||||||
|
pub fn render_swagger_callback(ApiDocContext) { "swagger/oauth2-redirect.html" }
|
||||||
|
|
||||||
/// Render the login page
|
/// Render the login page
|
||||||
pub fn render_login(WithLanguage<WithCsrf<LoginContext>>) { "pages/login.html" }
|
pub fn render_login(WithLanguage<WithCsrf<LoginContext>>) { "pages/login.html" }
|
||||||
|
|
||||||
@ -423,6 +429,8 @@ impl Templates {
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
check::render_not_found(self, now, rng)?;
|
check::render_not_found(self, now, rng)?;
|
||||||
check::render_app(self, now, rng)?;
|
check::render_app(self, now, rng)?;
|
||||||
|
check::render_swagger(self, now, rng)?;
|
||||||
|
check::render_swagger_callback(self, now, rng)?;
|
||||||
check::render_login(self, now, rng)?;
|
check::render_login(self, now, rng)?;
|
||||||
check::render_register(self, now, rng)?;
|
check::render_register(self, now, rng)?;
|
||||||
check::render_consent(self, now, rng)?;
|
check::render_consent(self, now, rng)?;
|
||||||
|
24
docs/api/index.html
Normal file
24
docs/api/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="description" content="SwaggerUI" />
|
||||||
|
<title>API documentation</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="swagger-ui"></div>
|
||||||
|
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script>
|
||||||
|
<script>
|
||||||
|
window.onload = () => {
|
||||||
|
window.ui = SwaggerUIBundle({
|
||||||
|
url: './spec.json',
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
presets: [
|
||||||
|
SwaggerUIBundle.presets.apis,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
80
docs/api/oauth2-redirect.html
Normal file
80
docs/api/oauth2-redirect.html
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<!-- This is taken from the swagger-ui/dist/oauth2-redirect.html file -->
|
||||||
|
<head>
|
||||||
|
<title>API documentation: OAuth2 Redirect</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
function run () {
|
||||||
|
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||||
|
var sentState = oauth2.state;
|
||||||
|
var redirectUrl = oauth2.redirectUrl;
|
||||||
|
var isValid, qp, arr;
|
||||||
|
|
||||||
|
if (/code|token|error/.test(window.location.hash)) {
|
||||||
|
qp = window.location.hash.substring(1).replace('?', '&');
|
||||||
|
} else {
|
||||||
|
qp = location.search.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
arr = qp.split("&");
|
||||||
|
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||||
|
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||||
|
function (key, value) {
|
||||||
|
return key === "" ? value : decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
) : {};
|
||||||
|
|
||||||
|
isValid = qp.state === sentState;
|
||||||
|
|
||||||
|
if ((
|
||||||
|
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||||
|
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||||
|
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||||
|
) && !oauth2.auth.code) {
|
||||||
|
if (!isValid) {
|
||||||
|
oauth2.errCb({
|
||||||
|
authId: oauth2.auth.name,
|
||||||
|
source: "auth",
|
||||||
|
level: "warning",
|
||||||
|
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qp.code) {
|
||||||
|
delete oauth2.state;
|
||||||
|
oauth2.auth.code = qp.code;
|
||||||
|
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||||
|
} else {
|
||||||
|
let oauthErrorMsg;
|
||||||
|
if (qp.error) {
|
||||||
|
oauthErrorMsg = "["+qp.error+"]: " +
|
||||||
|
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||||
|
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2.errCb({
|
||||||
|
authId: oauth2.auth.name,
|
||||||
|
source: "auth",
|
||||||
|
level: "error",
|
||||||
|
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||||
|
}
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
run();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
2365
frontend/package-lock.json
generated
2365
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -37,6 +37,7 @@
|
|||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-i18next": "^14.1.2",
|
"react-i18next": "^14.1.2",
|
||||||
|
"swagger-ui-react": "^5.17.14",
|
||||||
"urql": "^4.1.0",
|
"urql": "^4.1.0",
|
||||||
"vaul": "^0.9.1",
|
"vaul": "^0.9.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
@ -60,6 +61,7 @@
|
|||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/react-test-renderer": "^18.3.0",
|
"@types/react-test-renderer": "^18.3.0",
|
||||||
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"@vitest/coverage-v8": "^1.6.0",
|
"@vitest/coverage-v8": "^1.6.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
|
35
frontend/src/swagger.tsx
Normal file
35
frontend/src/swagger.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import SwaggerUI from "swagger-ui-react";
|
||||||
|
import "swagger-ui-react/swagger-ui.css";
|
||||||
|
|
||||||
|
type ApiConfig = {
|
||||||
|
openapiUrl: string;
|
||||||
|
callbackUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IWindow {
|
||||||
|
API_CONFIG?: ApiConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = typeof window !== "undefined" && (window as IWindow).API_CONFIG;
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("API_CONFIG is not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<SwaggerUI url={config.openapiUrl} oauth2RedirectUrl={config.callbackUrl} />,
|
||||||
|
);
|
@ -65,6 +65,7 @@ export default defineConfig((env) => ({
|
|||||||
resolve(__dirname, "src/main.tsx"),
|
resolve(__dirname, "src/main.tsx"),
|
||||||
resolve(__dirname, "src/shared.css"),
|
resolve(__dirname, "src/shared.css"),
|
||||||
resolve(__dirname, "src/templates.css"),
|
resolve(__dirname, "src/templates.css"),
|
||||||
|
resolve(__dirname, "src/swagger.tsx"),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -5,7 +5,7 @@ set -eu
|
|||||||
export SQLX_OFFLINE=1
|
export SQLX_OFFLINE=1
|
||||||
BASE_DIR="$(dirname "$0")/.."
|
BASE_DIR="$(dirname "$0")/.."
|
||||||
CONFIG_SCHEMA="${BASE_DIR}/docs/config.schema.json"
|
CONFIG_SCHEMA="${BASE_DIR}/docs/config.schema.json"
|
||||||
API_SCHEMA="${BASE_DIR}/docs/api.schema.json"
|
API_SCHEMA="${BASE_DIR}/docs/api/spec.json"
|
||||||
GRAPHQL_SCHEMA="${BASE_DIR}/frontend/schema.graphql"
|
GRAPHQL_SCHEMA="${BASE_DIR}/frontend/schema.graphql"
|
||||||
POLICIES_SCHEMA="${BASE_DIR}/policies/schema/"
|
POLICIES_SCHEMA="${BASE_DIR}/policies/schema/"
|
||||||
|
|
||||||
|
35
templates/swagger/doc.html
Normal file
35
templates/swagger/doc.html
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{#
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
#}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>API documentation</title>
|
||||||
|
<script>
|
||||||
|
window.API_CONFIG = {
|
||||||
|
openapiUrl: "{{ openapi_url | add_slashes | safe }}",
|
||||||
|
callbackUrl: "{{ callback_url | add_slashes | safe }}",
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
{{ include_asset('src/swagger.tsx') | indent(4) | safe }}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
97
templates/swagger/oauth2-redirect.html
Normal file
97
templates/swagger/oauth2-redirect.html
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
{#
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
#}
|
||||||
|
|
||||||
|
{# This is taken from the swagger-ui/dist/oauth2-redirect.html file #}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>API documentation: OAuth2 Redirect</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
function run () {
|
||||||
|
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||||
|
var sentState = oauth2.state;
|
||||||
|
var redirectUrl = oauth2.redirectUrl;
|
||||||
|
var isValid, qp, arr;
|
||||||
|
|
||||||
|
if (/code|token|error/.test(window.location.hash)) {
|
||||||
|
qp = window.location.hash.substring(1).replace('?', '&');
|
||||||
|
} else {
|
||||||
|
qp = location.search.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
arr = qp.split("&");
|
||||||
|
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
||||||
|
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||||
|
function (key, value) {
|
||||||
|
return key === "" ? value : decodeURIComponent(value);
|
||||||
|
}
|
||||||
|
) : {};
|
||||||
|
|
||||||
|
isValid = qp.state === sentState;
|
||||||
|
|
||||||
|
if ((
|
||||||
|
oauth2.auth.schema.get("flow") === "accessCode" ||
|
||||||
|
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
||||||
|
oauth2.auth.schema.get("flow") === "authorization_code"
|
||||||
|
) && !oauth2.auth.code) {
|
||||||
|
if (!isValid) {
|
||||||
|
oauth2.errCb({
|
||||||
|
authId: oauth2.auth.name,
|
||||||
|
source: "auth",
|
||||||
|
level: "warning",
|
||||||
|
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qp.code) {
|
||||||
|
delete oauth2.state;
|
||||||
|
oauth2.auth.code = qp.code;
|
||||||
|
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||||
|
} else {
|
||||||
|
let oauthErrorMsg;
|
||||||
|
if (qp.error) {
|
||||||
|
oauthErrorMsg = "["+qp.error+"]: " +
|
||||||
|
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||||
|
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
oauth2.errCb({
|
||||||
|
authId: oauth2.auth.name,
|
||||||
|
source: "auth",
|
||||||
|
level: "error",
|
||||||
|
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||||
|
}
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
run();
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user