Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit 31c94bc

Browse files
committed
Merge branch 'quenting/admin-api/doc' into quenting/admin-api/temp-merge-base
2 parents 53061ff + 2716fb0 commit 31c94bc

File tree

19 files changed

+2920
-290
lines changed

19 files changed

+2920
-290
lines changed

crates/handlers/src/admin/mod.rs

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,23 @@
1414

1515
use aide::{
1616
axum::ApiRouter,
17-
openapi::{OAuth2Flow, OAuth2Flows, OpenApi, SecurityScheme, Server, ServerVariable},
17+
openapi::{OAuth2Flow, OAuth2Flows, OpenApi, SecurityScheme, Server, Tag},
18+
};
19+
use axum::{
20+
extract::{FromRef, FromRequestParts, State},
21+
http::HeaderName,
22+
response::Html,
23+
Json, Router,
1824
};
19-
use axum::{extract::FromRequestParts, Json, Router};
2025
use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
2126
use indexmap::IndexMap;
27+
use mas_axum_utils::FancyError;
2228
use mas_http::CorsLayerExt;
23-
use mas_router::{OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, SimpleRoute};
29+
use mas_router::{
30+
ApiDoc, ApiDocCallback, OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, Route, SimpleRoute,
31+
UrlBuilder,
32+
};
33+
use mas_templates::{ApiDocContext, Templates};
2434
use tower_http::cors::{Any, CorsLayer};
2535

2636
mod call_context;
@@ -35,12 +45,19 @@ pub fn router<S>() -> (OpenApi, Router<S>)
3545
where
3646
S: Clone + Send + Sync + 'static,
3747
CallContext: FromRequestParts<S>,
48+
Templates: FromRef<S>,
49+
UrlBuilder: FromRef<S>,
3850
{
3951
let mut api = OpenApi::default();
4052
let router = ApiRouter::<S>::new()
4153
.nest("/api/admin/v1", self::v1::router())
4254
.finish_api_with(&mut api, |t| {
4355
t.title("Matrix Authentication Service admin API")
56+
.tag(Tag {
57+
name: "user".to_owned(),
58+
description: Some("Manage users".to_owned()),
59+
..Tag::default()
60+
})
4461
.security_scheme(
4562
"oauth2",
4663
SecurityScheme::OAuth2 {
@@ -70,34 +87,62 @@ where
7087
},
7188
)
7289
.security_requirement_scopes("oauth2", ["urn:mas:admin"])
73-
.server(Server {
74-
url: "{base}".to_owned(),
75-
variables: IndexMap::from([(
76-
"base".to_owned(),
77-
ServerVariable {
78-
default: "/".to_owned(),
79-
..ServerVariable::default()
80-
},
81-
)]),
82-
..Server::default()
83-
})
8490
});
8591

8692
let router = router
8793
// Serve the OpenAPI spec as JSON
8894
.route(
8995
"/api/spec.json",
9096
axum::routing::get({
91-
let res = Json(api.clone());
92-
move || std::future::ready(res.clone())
97+
let api = api.clone();
98+
move |State(url_builder): State<UrlBuilder>| {
99+
// Let's set the servers to the HTTP base URL
100+
let mut api = api.clone();
101+
api.servers = vec![Server {
102+
url: url_builder.http_base().to_string(),
103+
..Server::default()
104+
}];
105+
106+
std::future::ready(Json(api))
107+
}
93108
}),
94109
)
110+
// Serve the Swagger API reference
111+
.route(ApiDoc::route(), axum::routing::get(swagger))
112+
.route(
113+
ApiDocCallback::route(),
114+
axum::routing::get(swagger_callback),
115+
)
95116
.layer(
96117
CorsLayer::new()
97118
.allow_origin(Any)
98119
.allow_methods(Any)
99-
.allow_otel_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]),
120+
.allow_otel_headers([
121+
AUTHORIZATION,
122+
ACCEPT,
123+
CONTENT_TYPE,
124+
// Swagger will send this header, so we have to allow it to avoid CORS errors
125+
HeaderName::from_static("x-requested-with"),
126+
]),
100127
);
101128

102129
(api, router)
103130
}
131+
132+
async fn swagger(
133+
State(url_builder): State<UrlBuilder>,
134+
State(templates): State<Templates>,
135+
) -> Result<Html<String>, FancyError> {
136+
let ctx = ApiDocContext::from_url_builder(&url_builder);
137+
let res = templates.render_swagger(&ctx)?;
138+
Ok(Html(res))
139+
}
140+
141+
async fn swagger_callback(
142+
State(url_builder): State<UrlBuilder>,
143+
State(templates): State<Templates>,
144+
) -> Result<Html<String>, FancyError> {
145+
let ctx = ApiDocContext::from_url_builder(&url_builder);
146+
let res = templates.render_swagger_callback(&ctx)?;
147+
Ok(Html(res))
148+
}

crates/handlers/src/admin/v1/users/by_username.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ pub struct UsernamePathParam {
5858

5959
pub fn doc(operation: TransformOperation) -> TransformOperation {
6060
operation
61-
.description("Get a user by its username (localpart)")
61+
.summary("Get a user by its username (localpart)")
62+
.tag("user")
6263
.response_with::<200, Json<SingleResponse<User>>, _>(|t| {
6364
let [sample, ..] = User::samples();
6465
let response =

crates/handlers/src/admin/v1/users/get.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ impl IntoResponse for RouteError {
5252

5353
pub fn doc(operation: TransformOperation) -> TransformOperation {
5454
operation
55-
.description("Get a user")
55+
.summary("Get a user")
56+
.tag("user")
5657
.response_with::<200, Json<SingleResponse<User>>, _>(|t| {
5758
let [sample, ..] = User::samples();
5859
let response = SingleResponse::new_canonical(sample);

crates/handlers/src/admin/v1/users/list.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ impl IntoResponse for RouteError {
100100

101101
pub fn doc(operation: TransformOperation) -> TransformOperation {
102102
operation
103-
.description("List users")
103+
.summary("List users")
104+
.tag("user")
104105
.response_with::<200, Json<PaginatedResponse<User>>, _>(|t| {
105106
let users = User::samples();
106107
let pagination = mas_storage::Pagination::first(users.len());

crates/handlers/src/bin/api-schema.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
use std::io::Write;
2525

26+
use aide::openapi::{Server, ServerVariable};
27+
use indexmap::IndexMap;
28+
2629
/// This is a dummy state, it should never be used.
2730
///
2831
/// We use it to generate the API schema, which doesn't execute any request.
@@ -58,10 +61,25 @@ macro_rules! impl_from_ref {
5861
impl_from_request_parts!(mas_storage::BoxRepository);
5962
impl_from_request_parts!(mas_storage::BoxClock);
6063
impl_from_request_parts!(mas_handlers::BoundActivityTracker);
61-
impl_from_ref!(mas_keystore::Keystore);
64+
impl_from_ref!(mas_router::UrlBuilder);
65+
impl_from_ref!(mas_templates::Templates);
6266

6367
fn main() -> Result<(), Box<dyn std::error::Error>> {
64-
let (api, _) = mas_handlers::admin_api_router::<DummyState>();
68+
let (mut api, _) = mas_handlers::admin_api_router::<DummyState>();
69+
70+
// Set the server list to a configurable base URL
71+
api.servers = vec![Server {
72+
url: "{base}".to_owned(),
73+
variables: IndexMap::from([(
74+
"base".to_owned(),
75+
ServerVariable {
76+
default: "/".to_owned(),
77+
..ServerVariable::default()
78+
},
79+
)]),
80+
..Server::default()
81+
}];
82+
6583
let mut stdout = std::io::stdout();
6684
serde_json::to_writer_pretty(&mut stdout, &api)?;
6785

crates/router/src/endpoints.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,3 +870,24 @@ pub struct GraphQLPlayground;
870870
impl SimpleRoute for GraphQLPlayground {
871871
const PATH: &'static str = "/graphql/playground";
872872
}
873+
874+
/// `GET /api/spec.json`
875+
pub struct ApiSpec;
876+
877+
impl SimpleRoute for ApiSpec {
878+
const PATH: &'static str = "/api/spec.json";
879+
}
880+
881+
/// `GET /api/doc/`
882+
pub struct ApiDoc;
883+
884+
impl SimpleRoute for ApiDoc {
885+
const PATH: &'static str = "/api/doc/";
886+
}
887+
888+
/// `GET /api/doc/oauth2-callback`
889+
pub struct ApiDocCallback;
890+
891+
impl SimpleRoute for ApiDocCallback {
892+
const PATH: &'static str = "/api/doc/oauth2-callback";
893+
}

crates/router/src/url_builder.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ pub struct UrlBuilder {
2828
}
2929

3030
impl UrlBuilder {
31-
fn absolute_url_for<U>(&self, destination: &U) -> Url
31+
/// Create an absolute URL for a route
32+
#[must_use]
33+
pub fn absolute_url_for<U>(&self, destination: &U) -> Url
3234
where
3335
U: Route,
3436
{

crates/templates/src/context.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,35 @@ impl TemplateContext for AppContext {
337337
}
338338
}
339339

340+
/// Context used by the `swagger/doc.html` template
341+
#[derive(Serialize)]
342+
pub struct ApiDocContext {
343+
openapi_url: Url,
344+
callback_url: Url,
345+
}
346+
347+
impl ApiDocContext {
348+
/// Constructs a context for the API documentation page giben the
349+
/// [`UrlBuilder`]
350+
#[must_use]
351+
pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
352+
Self {
353+
openapi_url: url_builder.absolute_url_for(&mas_router::ApiSpec),
354+
callback_url: url_builder.absolute_url_for(&mas_router::ApiDocCallback),
355+
}
356+
}
357+
}
358+
359+
impl TemplateContext for ApiDocContext {
360+
fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
361+
where
362+
Self: Sized,
363+
{
364+
let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
365+
vec![Self::from_url_builder(&url_builder)]
366+
}
367+
}
368+
340369
/// Fields of the login form
341370
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
342371
#[serde(rename_all = "snake_case")]

crates/templates/src/lib.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,16 @@ mod macros;
4242

4343
pub use self::{
4444
context::{
45-
AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext, DeviceLinkContext,
46-
DeviceLinkFormField, EmailAddContext, EmailRecoveryContext, EmailVerificationContext,
47-
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
48-
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
49-
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryExpiredContext,
50-
RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext,
51-
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
52-
SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext, UpstreamExistingLinkContext,
53-
UpstreamRegister, UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf,
54-
WithLanguage, WithOptionalSession, WithSession,
45+
ApiDocContext, AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext,
46+
DeviceLinkContext, DeviceLinkFormField, EmailAddContext, EmailRecoveryContext,
47+
EmailVerificationContext, EmailVerificationPageContext, EmptyContext, ErrorContext,
48+
FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
49+
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
50+
ReauthFormField, RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
51+
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
52+
RegisterFormField, SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext,
53+
UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
54+
UpstreamSuggestLink, WithCaptcha, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
5555
},
5656
forms::{FieldError, FormError, FormField, FormState, ToFormState},
5757
};
@@ -324,6 +324,12 @@ register_templates! {
324324
/// Render the frontend app
325325
pub fn render_app(WithLanguage<AppContext>) { "app.html" }
326326

327+
/// Render the Swagger API reference
328+
pub fn render_swagger(ApiDocContext) { "swagger/doc.html" }
329+
330+
/// Render the Swagger OAuth2 callback page
331+
pub fn render_swagger_callback(ApiDocContext) { "swagger/oauth2-redirect.html" }
332+
327333
/// Render the login page
328334
pub fn render_login(WithLanguage<WithCsrf<LoginContext>>) { "pages/login.html" }
329335

@@ -423,6 +429,8 @@ impl Templates {
423429
) -> anyhow::Result<()> {
424430
check::render_not_found(self, now, rng)?;
425431
check::render_app(self, now, rng)?;
432+
check::render_swagger(self, now, rng)?;
433+
check::render_swagger_callback(self, now, rng)?;
426434
check::render_login(self, now, rng)?;
427435
check::render_register(self, now, rng)?;
428436
check::render_consent(self, now, rng)?;

docs/api/index.html

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="utf-8" />
4+
<meta name="viewport" content="width=device-width, initial-scale=1" />
5+
<meta name="description" content="SwaggerUI" />
6+
<title>API documentation</title>
7+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/swagger-ui.css" />
8+
</head>
9+
<body>
10+
<div id="swagger-ui"></div>
11+
<script src="https://unpkg.com/[email protected]/swagger-ui-bundle.js" crossorigin></script>
12+
<script>
13+
window.onload = () => {
14+
window.ui = SwaggerUIBundle({
15+
url: './spec.json',
16+
dom_id: '#swagger-ui',
17+
presets: [
18+
SwaggerUIBundle.presets.apis,
19+
],
20+
});
21+
};
22+
</script>
23+
</body>
24+
</html>

0 commit comments

Comments
 (0)