1
0
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:
Quentin Gliech
2024-07-26 17:09:42 +02:00
parent 70222eeb19
commit 3f947025e2
16 changed files with 2713 additions and 106 deletions

View File

@ -14,13 +14,23 @@
use aide::{
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 indexmap::IndexMap;
use mas_axum_utils::FancyError;
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};
mod call_context;
@ -35,6 +45,8 @@ pub fn router<S>() -> (OpenApi, Router<S>)
where
S: Clone + Send + Sync + 'static,
CallContext: FromRequestParts<S>,
Templates: FromRef<S>,
UrlBuilder: FromRef<S>,
{
let mut api = OpenApi::default();
let router = ApiRouter::<S>::new()
@ -70,17 +82,6 @@ where
},
)
.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
@ -88,16 +89,55 @@ where
.route(
"/api/spec.json",
axum::routing::get({
let res = Json(api.clone());
move || std::future::ready(res.clone())
let api = api.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(
CorsLayer::new()
.allow_origin(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)
}
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))
}

View File

@ -23,6 +23,9 @@
use std::io::Write;
use aide::openapi::{Server, ServerVariable};
use indexmap::IndexMap;
/// This is a dummy state, it should never be used.
///
/// 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::BoxClock);
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>> {
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();
serde_json::to_writer_pretty(&mut stdout, &api)?;

View File

@ -870,3 +870,24 @@ pub struct GraphQLPlayground;
impl SimpleRoute for GraphQLPlayground {
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";
}

View File

@ -28,7 +28,9 @@ pub struct 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
U: Route,
{

View File

@ -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
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]

View File

@ -42,16 +42,16 @@ mod macros;
pub use self::{
context::{
AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext, DeviceLinkContext,
DeviceLinkFormField, EmailAddContext, EmailRecoveryContext, EmailVerificationContext,
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryExpiredContext,
RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext,
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext, UpstreamExistingLinkContext,
UpstreamRegister, UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf,
WithLanguage, WithOptionalSession, WithSession,
ApiDocContext, AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext,
DeviceLinkContext, DeviceLinkFormField, EmailAddContext, EmailRecoveryContext,
EmailVerificationContext, EmailVerificationPageContext, EmptyContext, ErrorContext,
FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
ReauthFormField, RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
RegisterFormField, SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext,
UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
UpstreamSuggestLink, WithCaptcha, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
},
forms::{FieldError, FormError, FormField, FormState, ToFormState},
};
@ -324,6 +324,12 @@ register_templates! {
/// Render the frontend app
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
pub fn render_login(WithLanguage<WithCsrf<LoginContext>>) { "pages/login.html" }
@ -423,6 +429,8 @@ impl Templates {
) -> anyhow::Result<()> {
check::render_not_found(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_register(self, now, rng)?;
check::render_consent(self, now, rng)?;