1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +03:00

Allow running the authentication service on a different base path

This commit is contained in:
Quentin Gliech
2023-09-05 17:51:50 +02:00
parent 9ecb666ec1
commit 9b5c8fb44b
40 changed files with 388 additions and 195 deletions

View File

@ -57,11 +57,6 @@ impl Options {
let span = info_span!("cli.run.init").entered();
let config: AppConfig = root.load_config()?;
// XXX: there should be a generic config verification step
if config.http.public_base.path() != "/" {
anyhow::bail!("The http.public_base path is not set to /, this is not supported");
}
// Connect to the database
info!("Connecting to the database");
let pool = database_pool_from_config(&config.database).await?;
@ -201,19 +196,22 @@ impl Options {
let router = crate::server::build_router(
state.clone(),
&config.resources,
config.prefix.as_deref(),
config.name.as_deref(),
);
// Display some informations about where we'll be serving connections
let proto = if config.tls.is_some() { "https" } else { "http" };
let prefix = config.prefix.unwrap_or_default();
let addresses= listeners
.iter()
.map(|listener| {
if let Ok(addr) = listener.local_addr() {
format!("{proto}://{addr:?}")
format!("{proto}://{addr:?}{prefix}")
} else {
warn!("Could not get local address for listener, something might be wrong!");
format!("{proto}://???")
format!("{proto}://???{prefix}")
}
})
.join(", ");

View File

@ -175,6 +175,7 @@ fn on_http_response_labels<B>(res: &Response<B>) -> Vec<KeyValue> {
pub fn build_router<B>(
state: AppState,
resources: &[HttpResource],
prefix: Option<&str>,
name: Option<&str>,
) -> Router<(), B>
where
@ -244,6 +245,11 @@ where
}
}
if let Some(prefix) = prefix {
let path = format!("{}/", prefix.trim_end_matches('/'));
router = Router::new().nest(&path, router);
}
router = router.fallback(mas_handlers::fallback);
router

View File

@ -312,6 +312,10 @@ pub struct ListenerConfig {
/// List of resources to mount
pub resources: Vec<Resource>,
/// HTTP prefix to mount the resources on
#[serde(default)]
pub prefix: Option<String>,
/// List of sockets to bind
pub binds: Vec<BindConfig>,
@ -359,6 +363,7 @@ impl Default for HttpConfig {
path: http_listener_assets_path_default(),
},
],
prefix: None,
tls: None,
proxy_protocol: false,
binds: vec![BindConfig::Address {
@ -368,6 +373,7 @@ impl Default for HttpConfig {
ListenerConfig {
name: Some("internal".to_owned()),
resources: vec![Resource::Health],
prefix: None,
tls: None,
proxy_protocol: false,
binds: vec![BindConfig::Listen {

View File

@ -27,7 +27,7 @@ use mas_axum_utils::{
FancyError, SessionInfoExt,
};
use mas_data_model::Device;
use mas_router::{CompatLoginSsoAction, PostAuthAction, Route};
use mas_router::{CompatLoginSsoAction, PostAuthAction, UrlBuilder};
use mas_storage::{
compat::{CompatSessionRepository, CompatSsoLoginRepository},
job::{JobRepositoryExt, ProvisionDeviceJob},
@ -65,6 +65,7 @@ pub async fn get(
clock: BoxClock,
mut repo: BoxRepository,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
cookie_jar: CookieJar,
Path(id): Path<Ulid>,
Query(params): Query<Params>,
@ -78,10 +79,10 @@ pub async fn get(
// If there is no session, redirect to the login or register screen
let url = match params.action {
Some(CompatLoginSsoAction::Register) => {
mas_router::Register::and_continue_compat_sso_login(id).go()
url_builder.redirect(&mas_router::Register::and_continue_compat_sso_login(id))
}
Some(CompatLoginSsoAction::Login) | None => {
mas_router::Login::and_continue_compat_sso_login(id).go()
url_builder.redirect(&mas_router::Login::and_continue_compat_sso_login(id))
}
};
@ -92,7 +93,7 @@ pub async fn get(
if session.user.primary_user_email_id.is_none() {
let destination = mas_router::AccountAddEmail::default()
.and_then(PostAuthAction::continue_compat_sso_login(id));
return Ok((cookie_jar, destination.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
}
let login = repo
@ -134,6 +135,7 @@ pub async fn post(
mut repo: BoxRepository,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
cookie_jar: CookieJar,
Path(id): Path<Ulid>,
Query(params): Query<Params>,
@ -148,10 +150,10 @@ pub async fn post(
// If there is no session, redirect to the login or register screen
let url = match params.action {
Some(CompatLoginSsoAction::Register) => {
mas_router::Register::and_continue_compat_sso_login(id).go()
url_builder.redirect(&mas_router::Register::and_continue_compat_sso_login(id))
}
Some(CompatLoginSsoAction::Login) | None => {
mas_router::Login::and_continue_compat_sso_login(id).go()
url_builder.redirect(&mas_router::Login::and_continue_compat_sso_login(id))
}
};
@ -162,7 +164,7 @@ pub async fn post(
if session.user.primary_user_email_id.is_none() {
let destination = mas_router::AccountAddEmail::default()
.and_then(PostAuthAction::continue_compat_sso_login(id));
return Ok((cookie_jar, destination.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
}
let login = repo

View File

@ -30,7 +30,7 @@
clippy::let_with_type_underscore,
)]
use std::{borrow::Cow, convert::Infallible, time::Duration};
use std::{convert::Infallible, time::Duration};
use axum::{
body::{Bytes, HttpBody},
@ -276,6 +276,18 @@ where
mas_router::CompatRefresh::route(),
post(self::compat::refresh::post),
)
.route(
mas_router::CompatLoginSsoRedirect::route(),
get(self::compat::login_sso_redirect::get),
)
.route(
mas_router::CompatLoginSsoRedirectIdp::route(),
get(self::compat::login_sso_redirect::get),
)
.route(
mas_router::CompatLoginSsoRedirectSlash::route(),
get(self::compat::login_sso_redirect::get),
)
.layer(
CorsLayer::new()
.allow_origin(Any)
@ -318,16 +330,19 @@ where
// XXX: hard-coded redirect from /account to /account/
.route(
"/account",
get(|RawQuery(query): RawQuery| async {
let route = mas_router::Account::route();
let destination = if let Some(query) = query {
Cow::Owned(format!("{route}?{query}"))
} else {
Cow::Borrowed(route)
};
get(
|State(url_builder): State<UrlBuilder>, RawQuery(query): RawQuery| async move {
let prefix = url_builder.prefix().unwrap_or_default();
let route = mas_router::Account::route();
let destination = if let Some(query) = query {
format!("{prefix}{route}?{query}")
} else {
format!("{prefix}{route}")
};
axum::response::Redirect::to(&destination)
}),
axum::response::Redirect::to(&destination)
},
),
)
.route(mas_router::Account::route(), get(self::views::app::get))
.route(
@ -336,7 +351,9 @@ where
)
.route(
mas_router::ChangePasswordDiscovery::route(),
get(|| async { mas_router::AccountPassword.go() }),
get(|State(url_builder): State<UrlBuilder>| async move {
url_builder.redirect(&mas_router::AccountPassword)
}),
)
.route(mas_router::Index::route(), get(self::views::index::get))
.route(
@ -378,18 +395,6 @@ where
mas_router::Consent::route(),
get(self::oauth2::consent::get).post(self::oauth2::consent::post),
)
.route(
mas_router::CompatLoginSsoRedirect::route(),
get(self::compat::login_sso_redirect::get),
)
.route(
mas_router::CompatLoginSsoRedirectIdp::route(),
get(self::compat::login_sso_redirect::get),
)
.route(
mas_router::CompatLoginSsoRedirectSlash::route(),
get(self::compat::login_sso_redirect::get),
)
.route(
mas_router::CompatLoginSsoComplete::route(),
get(self::compat::login_sso_complete::get).post(self::compat::login_sso_complete::post),

View File

@ -21,7 +21,7 @@ use mas_axum_utils::{cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID, S
use mas_data_model::{AuthorizationGrant, BrowserSession, Client, Device};
use mas_keystore::Keystore;
use mas_policy::{EvaluationResult, Policy};
use mas_router::{PostAuthAction, Route, UrlBuilder};
use mas_router::{PostAuthAction, UrlBuilder};
use mas_storage::{
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository, OAuth2SessionRepository},
user::BrowserSessionRepository,
@ -117,7 +117,11 @@ pub(crate) async fn get(
let Some(session) = maybe_session else {
// If there is no session, redirect to the login screen, redirecting here after
// logout
return Ok((cookie_jar, mas_router::Login::and_then(continue_grant).go()).into_response());
return Ok((
cookie_jar,
url_builder.redirect(&mas_router::Login::and_then(continue_grant)),
)
.into_response());
};
activity_tracker
@ -137,7 +141,7 @@ pub(crate) async fn get(
repo,
key_store,
policy,
url_builder,
&url_builder,
grant,
&client,
&session,
@ -150,12 +154,12 @@ pub(crate) async fn get(
}
Err(GrantCompletionError::RequiresReauth) => Ok((
cookie_jar,
mas_router::Reauth::and_then(continue_grant).go(),
url_builder.redirect(&mas_router::Reauth::and_then(continue_grant)),
)
.into_response()),
Err(GrantCompletionError::RequiresConsent) => {
let next = mas_router::Consent(grant_id);
Ok((cookie_jar, next.go()).into_response())
Ok((cookie_jar, url_builder.redirect(&next)).into_response())
}
Err(GrantCompletionError::PolicyViolation(grant, res)) => {
warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id);
@ -206,7 +210,7 @@ pub(crate) async fn complete(
mut repo: BoxRepository,
key_store: Keystore,
mut policy: Policy,
url_builder: UrlBuilder,
url_builder: &UrlBuilder,
grant: AuthorizationGrant,
client: &Client,
browser_session: &BrowserSession,
@ -273,7 +277,7 @@ pub(crate) async fn complete(
params.id_token = Some(generate_id_token(
rng,
clock,
&url_builder,
url_builder,
&key_store,
client,
&grant,

View File

@ -21,7 +21,7 @@ use mas_axum_utils::{cookies::CookieJar, csrf::CsrfExt, sentry::SentryEventID, S
use mas_data_model::{AuthorizationCode, Pkce};
use mas_keystore::Keystore;
use mas_policy::Policy;
use mas_router::{PostAuthAction, Route, UrlBuilder};
use mas_router::{PostAuthAction, UrlBuilder};
use mas_storage::{
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
BoxClock, BoxRepository, BoxRng,
@ -313,16 +313,14 @@ pub(crate) async fn get(
// Client asked for a registration, show the registration prompt
repo.save().await?;
mas_router::Register::and_then(continue_grant)
.go()
url_builder.redirect(&mas_router::Register::and_then(continue_grant))
.into_response()
}
None => {
// Other cases where we don't have a session, ask for a login
repo.save().await?;
mas_router::Login::and_then(continue_grant)
.go()
url_builder.redirect(&mas_router::Login::and_then(continue_grant))
.into_response()
}
@ -336,8 +334,7 @@ pub(crate) async fn get(
activity_tracker.record_browser_session(&clock, &session).await;
mas_router::Reauth::and_then(continue_grant)
.go()
url_builder.redirect(&mas_router::Reauth::and_then(continue_grant))
.into_response()
}
@ -353,7 +350,7 @@ pub(crate) async fn get(
repo,
key_store,
policy,
url_builder,
&url_builder,
grant,
&client,
&user_session,
@ -403,7 +400,7 @@ pub(crate) async fn get(
repo,
key_store,
policy,
url_builder,
&url_builder,
grant,
&client,
&user_session,
@ -412,7 +409,7 @@ pub(crate) async fn get(
{
Ok(params) => callback_destination.go(&templates, params).await?,
Err(GrantCompletionError::RequiresConsent) => {
mas_router::Consent(grant_id).go().into_response()
url_builder.redirect(&mas_router::Consent(grant_id)).into_response()
}
Err(GrantCompletionError::PolicyViolation(grant, res)) => {
warn!(violation = ?res, "Authorization grant for client {} denied by policy", client.id);
@ -426,8 +423,7 @@ pub(crate) async fn get(
Html(content).into_response()
}
Err(GrantCompletionError::RequiresReauth) => {
mas_router::Reauth::and_then(continue_grant)
.go()
url_builder.redirect(&mas_router::Reauth::and_then(continue_grant))
.into_response()
}
Err(GrantCompletionError::Internal(e)) => {

View File

@ -25,7 +25,7 @@ use mas_axum_utils::{
};
use mas_data_model::{AuthorizationGrantStage, Device};
use mas_policy::Policy;
use mas_router::{PostAuthAction, Route};
use mas_router::{PostAuthAction, UrlBuilder};
use mas_storage::{
oauth2::{OAuth2AuthorizationGrantRepository, OAuth2ClientRepository},
BoxClock, BoxRepository, BoxRng,
@ -84,6 +84,7 @@ pub(crate) async fn get(
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
mut policy: Policy,
mut repo: BoxRepository,
activity_tracker: BoundActivityTracker,
@ -142,7 +143,7 @@ pub(crate) async fn get(
}
} else {
let login = mas_router::Login::and_continue_grant(grant_id);
Ok((cookie_jar, login.go()).into_response())
Ok((cookie_jar, url_builder.redirect(&login)).into_response())
}
}
@ -159,6 +160,7 @@ pub(crate) async fn post(
mut repo: BoxRepository,
activity_tracker: BoundActivityTracker,
cookie_jar: CookieJar,
State(url_builder): State<UrlBuilder>,
Path(grant_id): Path<Ulid>,
Form(form): Form<ProtectedForm<()>>,
) -> Result<Response, RouteError> {
@ -177,7 +179,7 @@ pub(crate) async fn post(
let Some(session) = maybe_session else {
let login = mas_router::Login::and_then(next);
return Ok((cookie_jar, login.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
activity_tracker
@ -222,5 +224,5 @@ pub(crate) async fn post(
repo.save().await?;
Ok((cookie_jar, next.go_next()).into_response())
Ok((cookie_jar, next.go_next(&url_builder)).into_response())
}

View File

@ -25,7 +25,7 @@ use mas_keystore::{Encrypter, Keystore};
use mas_oidc_client::requests::{
authorization_code::AuthorizationValidationData, jose::JwtVerificationData,
};
use mas_router::{Route, UrlBuilder};
use mas_router::UrlBuilder;
use mas_storage::{
upstream_oauth2::{
UpstreamOAuthLinkRepository, UpstreamOAuthProviderRepository,
@ -268,6 +268,6 @@ pub(crate) async fn get(
Ok((
cookie_jar,
mas_router::UpstreamOAuth2Link::new(link.id).go(),
url_builder.redirect(&mas_router::UpstreamOAuth2Link::new(link.id)),
))
}

View File

@ -27,6 +27,7 @@ use mas_axum_utils::{
use mas_data_model::{UpstreamOAuthProviderImportPreference, User};
use mas_jose::jwt::Jwt;
use mas_policy::Policy;
use mas_router::UrlBuilder;
use mas_storage::{
job::{JobRepositoryExt, ProvisionUserJob},
upstream_oauth2::{UpstreamOAuthLinkRepository, UpstreamOAuthSessionRepository},
@ -191,6 +192,7 @@ pub(crate) async fn get(
mut repo: BoxRepository,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
cookie_jar: CookieJar,
user_agent: Option<TypedHeader<headers::UserAgent>>,
Path(link_id): Path<Ulid>,
@ -248,7 +250,7 @@ pub(crate) async fn get(
repo.save().await?;
post_auth_action.go_next().into_response()
post_auth_action.go_next(&url_builder).into_response()
}
(Some(user_session), Some(user_id)) => {
@ -311,7 +313,7 @@ pub(crate) async fn get(
repo.save().await?;
post_auth_action.go_next().into_response()
post_auth_action.go_next(&url_builder).into_response()
}
(None, None) => {
@ -383,6 +385,7 @@ pub(crate) async fn post(
cookie_jar: CookieJar,
user_agent: Option<TypedHeader<headers::UserAgent>>,
mut policy: Policy,
State(url_builder): State<UrlBuilder>,
Path(link_id): Path<Ulid>,
Form(form): Form<ProtectedForm<FormData>>,
) -> Result<impl IntoResponse, RouteError> {
@ -577,5 +580,5 @@ pub(crate) async fn post(
repo.save().await?;
Ok((cookie_jar, post_auth_action.go_next()))
Ok((cookie_jar, post_auth_action.go_next(&url_builder)))
}

View File

@ -22,7 +22,7 @@ use mas_axum_utils::{
FancyError, SessionInfoExt,
};
use mas_policy::Policy;
use mas_router::Route;
use mas_router::UrlBuilder;
use mas_storage::{
job::{JobRepositoryExt, VerifyEmailJob},
user::UserEmailRepository,
@ -44,6 +44,7 @@ pub(crate) async fn get(
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
activity_tracker: BoundActivityTracker,
mut repo: BoxRepository,
cookie_jar: CookieJar,
@ -55,7 +56,7 @@ pub(crate) async fn get(
let Some(session) = maybe_session else {
let login = mas_router::Login::default();
return Ok((cookie_jar, login.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
activity_tracker
@ -80,6 +81,7 @@ pub(crate) async fn post(
PreferredLanguage(locale): PreferredLanguage,
mut policy: Policy,
cookie_jar: CookieJar,
State(url_builder): State<UrlBuilder>,
activity_tracker: BoundActivityTracker,
Query(query): Query<OptionalPostAuthAction>,
Form(form): Form<ProtectedForm<EmailForm>>,
@ -91,7 +93,7 @@ pub(crate) async fn post(
let Some(session) = maybe_session else {
let login = mas_router::Login::default();
return Ok((cookie_jar, login.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
// XXX: we really should show human readable errors on the form here
@ -135,9 +137,9 @@ pub(crate) async fn post(
next
};
next.go()
url_builder.redirect(&next)
} else {
query.go_next_or_default(&mas_router::Account::default())
query.go_next_or_default(&url_builder, &mas_router::Account::default())
};
repo.save().await?;

View File

@ -22,7 +22,7 @@ use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt,
};
use mas_router::Route;
use mas_router::UrlBuilder;
use mas_storage::{
job::{JobRepositoryExt, ProvisionUserJob},
user::UserEmailRepository,
@ -50,6 +50,7 @@ pub(crate) async fn get(
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
activity_tracker: BoundActivityTracker,
mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>,
@ -63,7 +64,7 @@ pub(crate) async fn get(
let Some(session) = maybe_session else {
let login = mas_router::Login::default();
return Ok((cookie_jar, login.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
activity_tracker
@ -79,7 +80,7 @@ pub(crate) async fn get(
if user_email.confirmed_at.is_some() {
// This email was already verified, skip
let destination = query.go_next_or_default(&mas_router::Account::default());
let destination = query.go_next_or_default(&url_builder, &mas_router::Account::default());
return Ok((cookie_jar, destination).into_response());
}
@ -103,6 +104,7 @@ pub(crate) async fn post(
clock: BoxClock,
mut repo: BoxRepository,
cookie_jar: CookieJar,
State(url_builder): State<UrlBuilder>,
activity_tracker: BoundActivityTracker,
Query(query): Query<OptionalPostAuthAction>,
Path(id): Path<Ulid>,
@ -115,7 +117,7 @@ pub(crate) async fn post(
let Some(session) = maybe_session else {
let login = mas_router::Login::default();
return Ok((cookie_jar, login.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
let user_email = repo
@ -157,6 +159,6 @@ pub(crate) async fn post(
.record_browser_session(&clock, &session)
.await;
let destination = query.go_next_or_default(&mas_router::Account::default());
let destination = query.go_next_or_default(&url_builder, &mas_router::Account::default());
Ok((cookie_jar, destination).into_response())
}

View File

@ -26,7 +26,7 @@ use mas_axum_utils::{
use mas_data_model::BrowserSession;
use mas_i18n::DataLocale;
use mas_policy::Policy;
use mas_router::Route;
use mas_router::UrlBuilder;
use mas_storage::{
user::{BrowserSessionRepository, UserPasswordRepository},
BoxClock, BoxRepository, BoxRng, Clock,
@ -53,12 +53,15 @@ pub(crate) async fn get(
State(templates): State<Templates>,
State(password_manager): State<PasswordManager>,
activity_tracker: BoundActivityTracker,
State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository,
cookie_jar: CookieJar,
) -> Result<Response, FancyError> {
// If the password manager is disabled, we can go back to the account page.
if !password_manager.is_enabled() {
return Ok(mas_router::Account::default().go().into_response());
return Ok(url_builder
.redirect(&mas_router::Account::default())
.into_response());
}
let (session_info, cookie_jar) = cookie_jar.session_info();
@ -73,7 +76,7 @@ pub(crate) async fn get(
render(&mut rng, &clock, locale, templates, session, cookie_jar).await
} else {
let login = mas_router::Login::and_then(mas_router::PostAuthAction::ChangePassword);
Ok((cookie_jar, login.go()).into_response())
Ok((cookie_jar, url_builder.redirect(&login)).into_response())
}
}
@ -105,6 +108,7 @@ pub(crate) async fn post(
State(password_manager): State<PasswordManager>,
State(templates): State<Templates>,
activity_tracker: BoundActivityTracker,
State(url_builder): State<UrlBuilder>,
mut policy: Policy,
mut repo: BoxRepository,
cookie_jar: CookieJar,
@ -123,7 +127,7 @@ pub(crate) async fn post(
let Some(session) = maybe_session else {
let login = mas_router::Login::and_then(mas_router::PostAuthAction::ChangePassword);
return Ok((cookie_jar, login.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
let user_password = repo

View File

@ -17,7 +17,7 @@ use axum::{
response::{Html, IntoResponse},
};
use mas_axum_utils::{cookies::CookieJar, FancyError, SessionInfoExt};
use mas_router::{PostAuthAction, Route};
use mas_router::{PostAuthAction, UrlBuilder};
use mas_storage::{BoxClock, BoxRepository};
use mas_templates::{AppContext, TemplateContext, Templates};
@ -28,6 +28,7 @@ pub async fn get(
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
activity_tracker: BoundActivityTracker,
State(url_builder): State<UrlBuilder>,
action: Option<Query<mas_router::AccountAction>>,
mut repo: BoxRepository,
clock: BoxClock,
@ -41,7 +42,9 @@ pub async fn get(
let Some(session) = session else {
return Ok((
cookie_jar,
mas_router::Login::and_then(PostAuthAction::manage_account(action)).go(),
url_builder.redirect(&mas_router::Login::and_then(
PostAuthAction::manage_account(action),
)),
)
.into_response());
};
@ -50,7 +53,7 @@ pub async fn get(
.record_browser_session(&clock, &session)
.await;
let ctx = AppContext::default().with_language(locale);
let ctx = AppContext::from_url_builder(&url_builder).with_language(locale);
let content = templates.render_app(&ctx)?;
Ok((cookie_jar, Html(content)).into_response())

View File

@ -26,7 +26,7 @@ use mas_axum_utils::{
};
use mas_data_model::BrowserSession;
use mas_i18n::DataLocale;
use mas_router::{Route, UpstreamOAuth2Authorize};
use mas_router::{UpstreamOAuth2Authorize, UrlBuilder};
use mas_storage::{
upstream_oauth2::UpstreamOAuthProviderRepository,
user::{BrowserSessionRepository, UserPasswordRepository, UserRepository},
@ -59,6 +59,7 @@ pub(crate) async fn get(
PreferredLanguage(locale): PreferredLanguage,
State(password_manager): State<PasswordManager>,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository,
activity_tracker: BoundActivityTracker,
Query(query): Query<OptionalPostAuthAction>,
@ -74,7 +75,7 @@ pub(crate) async fn get(
.record_browser_session(&clock, &session)
.await;
let reply = query.go_next();
let reply = query.go_next(&url_builder);
return Ok((cookie_jar, reply).into_response());
};
@ -91,7 +92,7 @@ pub(crate) async fn get(
destination = destination.and_then(action);
};
return Ok((cookie_jar, destination.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
};
let content = render(
@ -117,6 +118,7 @@ pub(crate) async fn post(
PreferredLanguage(locale): PreferredLanguage,
State(password_manager): State<PasswordManager>,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository,
activity_tracker: BoundActivityTracker,
Query(query): Query<OptionalPostAuthAction>,
@ -185,7 +187,7 @@ pub(crate) async fn post(
.await;
let cookie_jar = cookie_jar.set_session(&session_info);
let reply = query.go_next();
let reply = query.go_next(&url_builder);
Ok((cookie_jar, reply).into_response())
}
Err(e) => {
@ -360,7 +362,7 @@ mod test {
let response = state.request(Request::get("/login").empty()).await;
response.assert_status(StatusCode::SEE_OTHER);
response.assert_header_value(LOCATION, &first_provider_login.relative_url());
response.assert_header_value(LOCATION, &first_provider_login.path_and_query());
// Adding a second provider should show a login page with both providers
let mut repo = state.repository().await.unwrap();
@ -391,13 +393,13 @@ mod test {
.contains(&escape_html(&first_provider.issuer)));
assert!(response
.body()
.contains(&escape_html(&first_provider_login.relative_url())));
.contains(&escape_html(&first_provider_login.path_and_query())));
assert!(response
.body()
.contains(&escape_html(&second_provider.issuer)));
assert!(response
.body()
.contains(&escape_html(&second_provider_login.relative_url())));
.contains(&escape_html(&second_provider_login.path_and_query())));
}
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]

View File

@ -12,13 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use axum::{extract::Form, response::IntoResponse};
use axum::{
extract::{Form, State},
response::IntoResponse,
};
use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt,
};
use mas_router::{PostAuthAction, Route};
use mas_router::{PostAuthAction, UrlBuilder};
use mas_storage::{user::BrowserSessionRepository, BoxClock, BoxRepository};
use crate::BoundActivityTracker;
@ -28,6 +31,7 @@ pub(crate) async fn post(
clock: BoxClock,
mut repo: BoxRepository,
cookie_jar: CookieJar,
State(url_builder): State<UrlBuilder>,
activity_tracker: BoundActivityTracker,
Form(form): Form<ProtectedForm<Option<PostAuthAction>>>,
) -> Result<impl IntoResponse, FancyError> {
@ -49,9 +53,9 @@ pub(crate) async fn post(
repo.save().await?;
let destination = if let Some(action) = form {
action.go_next()
action.go_next(&url_builder)
} else {
mas_router::Login::default().go()
url_builder.redirect(&mas_router::Login::default())
};
Ok((cookie_jar, destination))

View File

@ -23,7 +23,7 @@ use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt,
};
use mas_router::Route;
use mas_router::UrlBuilder;
use mas_storage::{
user::{BrowserSessionRepository, UserPasswordRepository},
BoxClock, BoxRepository, BoxRng,
@ -47,6 +47,7 @@ pub(crate) async fn get(
PreferredLanguage(locale): PreferredLanguage,
State(password_manager): State<PasswordManager>,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
activity_tracker: BoundActivityTracker,
mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>,
@ -54,7 +55,9 @@ pub(crate) async fn get(
) -> Result<Response, FancyError> {
if !password_manager.is_enabled() {
// XXX: do something better here
return Ok(mas_router::Account::default().go().into_response());
return Ok(url_builder
.redirect(&mas_router::Account::default())
.into_response());
}
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
@ -66,7 +69,7 @@ pub(crate) async fn get(
// If there is no session, redirect to the login screen, keeping the
// PostAuthAction
let login = mas_router::Login::from(query.post_auth_action);
return Ok((cookie_jar, login.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
activity_tracker
@ -95,6 +98,7 @@ pub(crate) async fn post(
mut rng: BoxRng,
clock: BoxClock,
State(password_manager): State<PasswordManager>,
State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>,
cookie_jar: CookieJar,
@ -115,7 +119,7 @@ pub(crate) async fn post(
// If there is no session, redirect to the login screen, keeping the
// PostAuthAction
let login = mas_router::Login::from(query.post_auth_action);
return Ok((cookie_jar, login.go()).into_response());
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
// Load the user password
@ -162,6 +166,6 @@ pub(crate) async fn post(
let cookie_jar = cookie_jar.set_session(&session);
repo.save().await?;
let reply = query.go_next();
let reply = query.go_next(&url_builder);
Ok((cookie_jar, reply).into_response())
}

View File

@ -29,7 +29,7 @@ use mas_axum_utils::{
};
use mas_i18n::DataLocale;
use mas_policy::Policy;
use mas_router::Route;
use mas_router::UrlBuilder;
use mas_storage::{
job::{JobRepositoryExt, ProvisionUserJob, VerifyEmailJob},
user::{BrowserSessionRepository, UserEmailRepository, UserPasswordRepository, UserRepository},
@ -64,6 +64,7 @@ pub(crate) async fn get(
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(password_manager): State<PasswordManager>,
State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository,
Query(query): Query<OptionalPostAuthAction>,
cookie_jar: CookieJar,
@ -74,14 +75,14 @@ pub(crate) async fn get(
let maybe_session = session_info.load_session(&mut repo).await?;
if maybe_session.is_some() {
let reply = query.go_next();
let reply = query.go_next(&url_builder);
return Ok((cookie_jar, reply).into_response());
}
if !password_manager.is_enabled() {
// If password-based login is disabled, redirect to the login page here
return Ok(mas_router::Login::from(query.post_auth_action)
.go()
return Ok(url_builder
.redirect(&mas_router::Login::from(query.post_auth_action))
.into_response());
}
@ -106,6 +107,7 @@ pub(crate) async fn post(
PreferredLanguage(locale): PreferredLanguage,
State(password_manager): State<PasswordManager>,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
mut policy: Policy,
mut repo: BoxRepository,
activity_tracker: BoundActivityTracker,
@ -239,7 +241,7 @@ pub(crate) async fn post(
.await;
let cookie_jar = cookie_jar.set_session(&session);
Ok((cookie_jar, next.go()).into_response())
Ok((cookie_jar, url_builder.redirect(&next)).into_response())
}
async fn render(
@ -282,12 +284,12 @@ mod tests {
state
};
let request = Request::get(&*mas_router::Register::default().relative_url()).empty();
let request = Request::get(&*mas_router::Register::default().path_and_query()).empty();
let response = state.request(request).await;
response.assert_status(StatusCode::SEE_OTHER);
response.assert_header_value(LOCATION, "/login");
let request = Request::post(&*mas_router::Register::default().relative_url()).form(
let request = Request::post(&*mas_router::Register::default().path_and_query()).form(
serde_json::json!({
"csrf": "abc",
"username": "john",

View File

@ -13,7 +13,7 @@
// limitations under the License.
use anyhow::Context;
use mas_router::{PostAuthAction, Route};
use mas_router::{PostAuthAction, Route, UrlBuilder};
use mas_storage::{
compat::CompatSsoLoginRepository,
oauth2::OAuth2AuthorizationGrantRepository,
@ -30,14 +30,19 @@ pub(crate) struct OptionalPostAuthAction {
}
impl OptionalPostAuthAction {
pub fn go_next_or_default<T: Route>(&self, default: &T) -> axum::response::Redirect {
self.post_auth_action
.as_ref()
.map_or_else(|| default.go(), mas_router::PostAuthAction::go_next)
pub fn go_next_or_default<T: Route>(
&self,
url_builder: &UrlBuilder,
default: &T,
) -> axum::response::Redirect {
self.post_auth_action.as_ref().map_or_else(
|| url_builder.redirect(default),
|action| action.go_next(url_builder),
)
}
pub fn go_next(&self) -> axum::response::Redirect {
self.go_next_or_default(&mas_router::Index)
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
self.go_next_or_default(url_builder, &mas_router::Index)
}
pub async fn load_context<'a>(

View File

@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
use ulid::Ulid;
pub use crate::traits::*;
use crate::UrlBuilder;
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "snake_case", tag = "kind")]
@ -57,16 +58,19 @@ impl PostAuthAction {
PostAuthAction::ManageAccount { action }
}
pub fn go_next(&self) -> axum::response::Redirect {
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
match self {
Self::ContinueAuthorizationGrant { id } => ContinueAuthorizationGrant(*id).go(),
Self::ContinueCompatSsoLogin { id } => CompatLoginSsoComplete::new(*id, None).go(),
Self::ChangePassword => AccountPassword.go(),
Self::LinkUpstream { id } => UpstreamOAuth2Link::new(*id).go(),
Self::ManageAccount { action } => Account {
action: action.clone(),
Self::ContinueAuthorizationGrant { id } => {
url_builder.redirect(&ContinueAuthorizationGrant(*id))
}
.go(),
Self::ContinueCompatSsoLogin { id } => {
url_builder.redirect(&CompatLoginSsoComplete::new(*id, None))
}
Self::ChangePassword => url_builder.redirect(&AccountPassword),
Self::LinkUpstream { id } => url_builder.redirect(&UpstreamOAuth2Link::new(*id)),
Self::ManageAccount { action } => url_builder.redirect(&Account {
action: action.clone(),
}),
}
}
}
@ -219,10 +223,10 @@ impl Login {
self.post_auth_action.as_ref()
}
pub fn go_next(&self) -> axum::response::Redirect {
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
match &self.post_auth_action {
Some(action) => action.go_next(),
None => Index.go(),
Some(action) => action.go_next(url_builder),
None => url_builder.redirect(&Index),
}
}
}
@ -268,10 +272,10 @@ impl Reauth {
self.post_auth_action.as_ref()
}
pub fn go_next(&self) -> axum::response::Redirect {
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
match &self.post_auth_action {
Some(action) => action.go_next(),
None => Index.go(),
Some(action) => action.go_next(url_builder),
None => url_builder.redirect(&Index),
}
}
}
@ -328,10 +332,10 @@ impl Register {
self.post_auth_action.as_ref()
}
pub fn go_next(&self) -> axum::response::Redirect {
pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
match &self.post_auth_action {
Some(action) => action.go_next(),
None => Index.go(),
Some(action) => action.go_next(url_builder),
None => url_builder.redirect(&Index),
}
}
}

View File

@ -38,12 +38,12 @@ mod tests {
#[test]
fn test_relative_urls() {
assert_eq!(
OidcConfiguration.relative_url(),
OidcConfiguration.path_and_query(),
Cow::Borrowed("/.well-known/openid-configuration")
);
assert_eq!(Index.relative_url(), Cow::Borrowed("/"));
assert_eq!(Index.path_and_query(), Cow::Borrowed("/"));
assert_eq!(
Login::and_continue_grant(Ulid::nil()).relative_url(),
Login::and_continue_grant(Ulid::nil()).path_and_query(),
Cow::Borrowed("/login?kind=continue_authorization_grant&id=00000000000000000000000000")
);
}

View File

@ -28,7 +28,7 @@ pub trait Route {
Cow::Borrowed(Self::route())
}
fn relative_url(&self) -> Cow<'static, str> {
fn path_and_query(&self) -> Cow<'static, str> {
let path = self.path();
if let Some(query) = self.query() {
let query = serde_urlencoded::to_string(query).unwrap();
@ -39,17 +39,10 @@ pub trait Route {
}
fn absolute_url(&self, base: &Url) -> Url {
let relative = self.relative_url();
let relative = self.path_and_query();
let relative = relative.trim_start_matches('/');
base.join(relative.borrow()).unwrap()
}
fn go(&self) -> axum::response::Redirect {
axum::response::Redirect::to(&self.relative_url())
}
fn go_absolute(&self, base: &Url) -> axum::response::Redirect {
axum::response::Redirect::to(self.absolute_url(base).as_str())
}
}
pub trait SimpleRoute {

View File

@ -14,8 +14,6 @@
//! Utility to build URLs
use std::borrow::Cow;
use ulid::Ulid;
use url::Url;
@ -24,32 +22,91 @@ use crate::traits::Route;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UrlBuilder {
http_base: Url,
assets_base: Cow<'static, str>,
prefix: String,
assets_base: String,
issuer: Url,
}
impl UrlBuilder {
fn url_for<U>(&self, destination: &U) -> Url
fn absolute_url_for<U>(&self, destination: &U) -> Url
where
U: Route,
{
destination.absolute_url(&self.http_base)
}
/// Create a relative URL for a route, prefixed with the base URL
#[must_use]
pub fn relative_url_for<U>(&self, destination: &U) -> String
where
U: Route,
{
format!(
"{prefix}{destination}",
prefix = self.prefix,
destination = destination.path_and_query()
)
}
/// The prefix added to all relative URLs
#[must_use]
pub fn prefix(&self) -> Option<&str> {
if self.prefix.is_empty() {
None
} else {
Some(&self.prefix)
}
}
/// Create a (relative) redirect response to a route
pub fn redirect<U>(&self, destination: &U) -> axum::response::Redirect
where
U: Route,
{
let uri = self.relative_url_for(destination);
axum::response::Redirect::to(&uri)
}
/// Create an absolute redirect response to a route
pub fn absolute_redirect<U>(&self, destination: &U) -> axum::response::Redirect
where
U: Route,
{
destination.go_absolute(&self.http_base)
let uri = self.absolute_url_for(destination);
axum::response::Redirect::to(uri.as_str())
}
/// Create a new [`UrlBuilder`] from a base URL
///
/// # Panics
///
/// Panics if the base URL contains a fragment, a query, credentials or
/// isn't HTTP/HTTPS;
#[must_use]
pub fn new(base: Url, issuer: Option<Url>, assets_base: Option<String>) -> Self {
assert!(
base.scheme() == "http" || base.scheme() == "https",
"base URL must be HTTP/HTTPS"
);
assert_eq!(base.query(), None, "base URL must not contain a query");
assert_eq!(
base.fragment(),
None,
"base URL must not contain a fragment"
);
assert_eq!(base.username(), "", "base URL must not contain credentials");
assert_eq!(
base.password(),
None,
"base URL must not contain credentials"
);
let issuer = issuer.unwrap_or_else(|| base.clone());
let assets_base = assets_base.map_or(Cow::Borrowed("/assets/"), Cow::Owned);
let prefix = base.path().trim_end_matches('/').to_owned();
let assets_base = assets_base.unwrap_or_else(|| format!("{prefix}/assets/"));
Self {
http_base: base,
prefix,
assets_base,
issuer,
}
@ -70,49 +127,49 @@ impl UrlBuilder {
/// OAuth 2.0 authorization endpoint
#[must_use]
pub fn oauth_authorization_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2AuthorizationEndpoint)
self.absolute_url_for(&crate::endpoints::OAuth2AuthorizationEndpoint)
}
/// OAuth 2.0 token endpoint
#[must_use]
pub fn oauth_token_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2TokenEndpoint)
self.absolute_url_for(&crate::endpoints::OAuth2TokenEndpoint)
}
/// OAuth 2.0 introspection endpoint
#[must_use]
pub fn oauth_introspection_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2Introspection)
self.absolute_url_for(&crate::endpoints::OAuth2Introspection)
}
/// OAuth 2.0 revocation endpoint
#[must_use]
pub fn oauth_revocation_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2Revocation)
self.absolute_url_for(&crate::endpoints::OAuth2Revocation)
}
/// OAuth 2.0 client registration endpoint
#[must_use]
pub fn oauth_registration_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2RegistrationEndpoint)
self.absolute_url_for(&crate::endpoints::OAuth2RegistrationEndpoint)
}
// OIDC userinfo endpoint
#[must_use]
pub fn oidc_userinfo_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::OidcUserinfo)
self.absolute_url_for(&crate::endpoints::OidcUserinfo)
}
/// JWKS URI
#[must_use]
pub fn jwks_uri(&self) -> Url {
self.url_for(&crate::endpoints::OAuth2Keys)
self.absolute_url_for(&crate::endpoints::OAuth2Keys)
}
/// Static asset
#[must_use]
pub fn static_asset(&self, path: String) -> Url {
self.url_for(&crate::endpoints::StaticAsset::new(path))
self.absolute_url_for(&crate::endpoints::StaticAsset::new(path))
}
/// Static asset base
@ -124,18 +181,83 @@ impl UrlBuilder {
/// GraphQL endpoint
#[must_use]
pub fn graphql_endpoint(&self) -> Url {
self.url_for(&crate::endpoints::GraphQL)
self.absolute_url_for(&crate::endpoints::GraphQL)
}
/// Upstream redirect URI
#[must_use]
pub fn upstream_oauth_callback(&self, id: Ulid) -> Url {
self.url_for(&crate::endpoints::UpstreamOAuth2Callback::new(id))
self.absolute_url_for(&crate::endpoints::UpstreamOAuth2Callback::new(id))
}
/// Upstream authorize URI
#[must_use]
pub fn upstream_oauth_authorize(&self, id: Ulid) -> Url {
self.url_for(&crate::endpoints::UpstreamOAuth2Authorize::new(id))
self.absolute_url_for(&crate::endpoints::UpstreamOAuth2Authorize::new(id))
}
}
#[cfg(test)]
mod tests {
#[test]
#[should_panic]
fn test_invalid_base_url_scheme() {
let _ = super::UrlBuilder::new(url::Url::parse("file:///tmp/").unwrap(), None, None);
}
#[test]
#[should_panic]
fn test_invalid_base_url_query() {
let _ = super::UrlBuilder::new(
url::Url::parse("https://example.com/?foo=bar").unwrap(),
None,
None,
);
}
#[test]
#[should_panic]
fn test_invalid_base_url_fragment() {
let _ = super::UrlBuilder::new(
url::Url::parse("https://example.com/#foo").unwrap(),
None,
None,
);
}
#[test]
#[should_panic]
fn test_invalid_base_url_credentials() {
let _ = super::UrlBuilder::new(
url::Url::parse("https://foo@example.com/").unwrap(),
None,
None,
);
}
#[test]
fn test_url_prefix() {
let builder = super::UrlBuilder::new(
url::Url::parse("https://example.com/foo/").unwrap(),
None,
None,
);
assert_eq!(builder.prefix, "/foo");
let builder =
super::UrlBuilder::new(url::Url::parse("https://example.com/").unwrap(), None, None);
assert_eq!(builder.prefix, "");
}
#[test]
fn test_absolute_uri_prefix() {
let builder = super::UrlBuilder::new(
url::Url::parse("https://example.com/foo/").unwrap(),
None,
None,
);
let uri = builder.absolute_url_for(&crate::endpoints::OAuth2AuthorizationEndpoint);
assert_eq!(uri.as_str(), "https://example.com/foo/authorize");
}
}

View File

@ -23,7 +23,7 @@ use mas_data_model::{
UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail, UserEmailVerification,
};
use mas_i18n::DataLocale;
use mas_router::{PostAuthAction, Route};
use mas_router::{Account, GraphQL, PostAuthAction, Route, UrlBuilder};
use rand::Rng;
use serde::{ser::SerializeStruct, Deserialize, Serialize};
use ulid::Ulid;
@ -276,30 +276,29 @@ impl TemplateContext for IndexContext {
/// Config used by the frontend app
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AppConfig {
root: String,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
root: "/account/".into(),
}
}
graphql_endpoint: String,
}
/// Context used by the `app.html` template
#[derive(Serialize, Default)]
#[derive(Serialize)]
pub struct AppContext {
app_config: AppConfig,
}
impl AppContext {
/// Constructs the context for the app page with the given app root
/// Constructs the context given the [`UrlBuilder`]
#[must_use]
pub fn with_app_root(root: String) -> Self {
pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
let root = url_builder.relative_url_for(&Account::default());
let graphql_endpoint = url_builder.relative_url_for(&GraphQL);
Self {
app_config: AppConfig { root },
app_config: AppConfig {
root,
graphql_endpoint,
},
}
}
}
@ -309,7 +308,8 @@ impl TemplateContext for AppContext {
where
Self: Sized,
{
vec![Self::default()]
let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
vec![Self::from_url_builder(&url_builder)]
}
}
@ -935,7 +935,7 @@ impl UpstreamRegister {
fn for_link_id(id: Ulid) -> Self {
let login_link = mas_router::Login::and_link_upstream(id)
.relative_url()
.path_and_query()
.into();
Self {

View File

@ -52,7 +52,7 @@ pub fn register(
env.add_global(
"include_asset",
Value::from_object(IncludeAsset {
url_builder,
url_builder: url_builder.clone(),
vite_manifest,
}),
);
@ -60,6 +60,19 @@ pub fn register(
"translator",
Value::from_object(TranslatorFunc { translator }),
);
env.add_filter("prefix_url", move |url: &str| -> String {
if !url.starts_with('/') {
// Let's assume it's not an internal URL and return it as-is
return url.to_owned();
}
let Some(prefix) = url_builder.prefix() else {
// If there is no prefix to add, return the URL as-is
return url.to_owned();
};
format!("{prefix}{url}")
});
}
fn tester_empty(seq: &dyn SeqObject) -> bool {

View File

@ -1278,6 +1278,10 @@
"description": "A unique name for this listener which will be shown in traces and in metrics labels",
"type": "string"
},
"prefix": {
"description": "HTTP prefix to mount the resources on",
"type": "string"
},
"proxy_protocol": {
"description": "Accept HAProxy's Proxy Protocol V1",
"default": false,

View File

@ -23,7 +23,7 @@ limitations under the License.
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>matrix-authentication-service</title>
<script type="application/javascript">
window.APP_CONFIG = JSON.parse('{"root": "/account/"}');
window.APP_CONFIG = JSON.parse('{"root": "/account/", "graphqlEndpoint": "/graphql"}');
(function () {
const query = window.matchMedia("(prefers-color-scheme: dark)");
function handleChange(list) {

View File

@ -38,7 +38,7 @@ const HydrateLocation: React.FC<React.PropsWithChildren<{ path: string }>> = ({
path,
}) => {
useHydrateAtoms([
[appConfigAtom, { root: "/" }],
[appConfigAtom, { root: "/", graphqlEndpoint: "/graphql" }],
[locationAtom, { pathname: path }],
]);
return <>{children}</>;

View File

@ -40,7 +40,7 @@ const meta = {
const WithHomePage: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
useHydrateAtoms([
[appConfigAtom, { root: "/" }],
[appConfigAtom, { root: "/", graphqlEndpoint: "/graphql" }],
[locationAtom, { pathname: "/" }],
]);
return <>{children}</>;

View File

@ -35,7 +35,7 @@ const meta = {
const WithHomePage: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
useHydrateAtoms([
[appConfigAtom, { root: "/" }],
[appConfigAtom, { root: "/", graphqlEndpoint: "/graphql" }],
[locationAtom, { pathname: "/" }],
]);
return <>{children}</>;

View File

@ -38,7 +38,7 @@ const HydrateLocation: React.FC<React.PropsWithChildren<{ path: string }>> = ({
path,
}) => {
useHydrateAtoms([
[appConfigAtom, { root: "/" }],
[appConfigAtom, { root: "/", graphqlEndpoint: "/graphql" }],
[locationAtom, { pathname: path }],
]);
return <>{children}</>;

View File

@ -27,7 +27,7 @@ type Props = {
const WithHomePage: React.FC<React.PropsWithChildren<{}>> = ({ children }) => {
useHydrateAtoms([
[appConfigAtom, { root: "/" }],
[appConfigAtom, { root: "/", graphqlEndpoint: "/graphql" }],
[locationAtom, { pathname: "/" }],
]);
return <>{children}</>;

View File

@ -12,10 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
type AppConfig = {
export type AppConfig = {
root: string;
graphqlEndpoint: string;
};
interface Window {
APP_CONFIG: AppConfig;
interface IWindow {
APP_CONFIG?: AppConfig;
}
const config: AppConfig = (typeof window !== "undefined" &&
(window as IWindow).APP_CONFIG) || { root: "/", graphqlEndpoint: "/graphql" };
export default config;

View File

@ -18,6 +18,7 @@ import { cacheExchange } from "@urql/exchange-graphcache";
import { refocusExchange } from "@urql/exchange-refocus";
import { requestPolicyExchange } from "@urql/exchange-request-policy";
import appConfig from "./config";
import type {
MutationAddEmailArgs,
MutationRemoveEmailArgs,
@ -130,7 +131,7 @@ const exchanges = [
];
export const client = createClient({
url: "/graphql",
url: appConfig.graphqlEndpoint,
// Add the devtools exchange in development
exchanges: import.meta.env.DEV ? [devtoolsExchange, ...exchanges] : exchanges,
});

View File

@ -15,11 +15,11 @@
import { atom } from "jotai";
import { atomWithLocation } from "jotai-location";
import appConfig, { AppConfig } from "../config";
import { Location, pathToRoute, Route, routeToPath } from "./routes";
export const appConfigAtom = atom<AppConfig>(
typeof window !== "undefined" ? window.APP_CONFIG : { root: "/" },
);
export const appConfigAtom = atom<AppConfig>(appConfig);
const locationToRoute = (root: string, location: Location): Route => {
if (!location.pathname || !location.pathname.startsWith(root)) {

View File

@ -24,7 +24,7 @@ const HydrateLocation: React.FC<React.PropsWithChildren<{ path: string }>> = ({
path,
}) => {
useHydrateAtoms([
[appConfigAtom, { root: "/" }],
[appConfigAtom, { root: "/", graphqlEndpoint: "/graphql" }],
[locationAtom, { pathname: path }],
]);
return <>{children}</>;

View File

@ -15,15 +15,15 @@ limitations under the License.
#}
{% macro link(text, href="#", class="") %}
<a class="cpd-button {{ class }}" data-kind="primary" data-size="lg" href="{{ href }}">{{ text }}</a>
<a class="cpd-button {{ class }}" data-kind="primary" data-size="lg" href="{{ href | prefix_url }}">{{ text }}</a>
{% endmacro %}
{% macro link_text(text, href="#", class="") %}
<a class="cpd-link {{ class }}" data-kind="primary" href="{{ href }}">{{ text }}</a>
<a class="cpd-link {{ class }}" data-kind="primary" href="{{ href | prefix_url }}">{{ text }}</a>
{% endmacro %}
{% macro link_outline(text, href="#", class="") %}
<a class="cpd-button {{ class }}" data-kind="secondary" data-size="lg" href="{{ href }}">{{ text }}</a>
<a class="cpd-button {{ class }}" data-kind="secondary" data-size="lg" href="{{ href | prefix_url }}">{{ text }}</a>
{% endmacro %}
{% macro button(

View File

@ -15,7 +15,7 @@ limitations under the License.
#}
{% macro button(text, csrf_token, as_link=false, post_logout_action={}) %}
<form method="POST" action="/logout" class="inline">
<form method="POST" action="{{ "/logout" | prefix_url }}" class="inline">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{% for key, value in post_logout_action|items %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />

View File

@ -22,7 +22,7 @@ limitations under the License.
<h1 class="text-xl font-semibold">{{ _("mas.not_found.heading") }}</h1>
<p>{{ _("mas.not_found.description") }}</p>
<div>
<a class="cpd-link" data-kind="primary" href="/">{{ _("mas.back_to_homepage") }}</a>
{{ button.link_text(text=_("mas.back_to_homepage"), href="/") }}
</div>
<hr />

View File

@ -77,7 +77,7 @@
},
"back_to_homepage": "Go back to the homepage",
"@back_to_homepage": {
"context": "pages/404.html:25:64-89"
"context": "pages/404.html:25:37-62"
},
"change_password": {
"change": "Change password",