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::{
|
||||
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))
|
||||
}
|
||||
|
@ -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)?;
|
||||
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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,
|
||||
{
|
||||
|
@ -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")]
|
||||
|
@ -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)?;
|
||||
|
Reference in New Issue
Block a user