You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
templates: replace tera with minijinja
This commit is contained in:
25
Cargo.lock
generated
25
Cargo.lock
generated
@@ -202,6 +202,12 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e"
|
checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2"
|
name = "argon2"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -3334,23 +3340,25 @@ name = "mas-templates"
|
|||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"arc-swap",
|
||||||
"camino",
|
"camino",
|
||||||
"chrono",
|
"chrono",
|
||||||
"http",
|
"http",
|
||||||
"mas-data-model",
|
"mas-data-model",
|
||||||
"mas-router",
|
"mas-router",
|
||||||
"mas-spa",
|
"mas-spa",
|
||||||
|
"minijinja",
|
||||||
"oauth2-types",
|
"oauth2-types",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"tera",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"ulid",
|
"ulid",
|
||||||
"url",
|
"url",
|
||||||
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3413,6 +3421,12 @@ dependencies = [
|
|||||||
"rustix 0.37.23",
|
"rustix 0.37.23",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memo-map"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "374c335b2df19e62d4cb323103473cbc6510980253119180de862d89184f6a83"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memoffset"
|
name = "memoffset"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -3444,7 +3458,10 @@ version = "1.0.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "80084fa3099f58b7afab51e5f92e24c2c2c68dcad26e96ad104bd6011570461d"
|
checksum = "80084fa3099f58b7afab51e5f92e24c2c2c68dcad26e96ad104bd6011570461d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"memo-map",
|
||||||
|
"self_cell",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4906,6 +4923,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "self_cell"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
|
22
Cargo.toml
22
Cargo.toml
@@ -17,16 +17,16 @@ repository = "https://github.com/matrix-org/matrix-authentication-service/"
|
|||||||
[workspace.dependencies.anyhow]
|
[workspace.dependencies.anyhow]
|
||||||
version = "1.0.75"
|
version = "1.0.75"
|
||||||
|
|
||||||
|
# UTF-8 paths
|
||||||
|
[workspace.dependencies.camino]
|
||||||
|
version = "1.1.6"
|
||||||
|
|
||||||
# Time utilities
|
# Time utilities
|
||||||
[workspace.dependencies.chrono]
|
[workspace.dependencies.chrono]
|
||||||
version = "0.4.31"
|
version = "0.4.31"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["serde", "clock"]
|
features = ["serde", "clock"]
|
||||||
|
|
||||||
# UTF-8 paths
|
|
||||||
[workspace.dependencies.camino]
|
|
||||||
version = "1.1.6"
|
|
||||||
|
|
||||||
# CLI argument parsing
|
# CLI argument parsing
|
||||||
[workspace.dependencies.clap]
|
[workspace.dependencies.clap]
|
||||||
version = "4.4.4"
|
version = "4.4.4"
|
||||||
@@ -36,6 +36,10 @@ features = ["derive"]
|
|||||||
[workspace.dependencies.http]
|
[workspace.dependencies.http]
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
[workspace.dependencies.minijinja]
|
||||||
|
version = "1.0.8"
|
||||||
|
|
||||||
# Random values
|
# Random values
|
||||||
[workspace.dependencies.rand]
|
[workspace.dependencies.rand]
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
@@ -49,6 +53,11 @@ features = ["derive"] # Most of the time, if we need serde, we need derive
|
|||||||
[workspace.dependencies.serde_json]
|
[workspace.dependencies.serde_json]
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
|
|
||||||
|
# Templates
|
||||||
|
[workspace.dependencies.tera]
|
||||||
|
version = "1.19.1"
|
||||||
|
default-features = false
|
||||||
|
|
||||||
# Custom error types
|
# Custom error types
|
||||||
[workspace.dependencies.thiserror]
|
[workspace.dependencies.thiserror]
|
||||||
version = "1.0.48"
|
version = "1.0.48"
|
||||||
@@ -59,11 +68,6 @@ version = "0.1.37"
|
|||||||
[workspace.dependencies.tracing-subscriber]
|
[workspace.dependencies.tracing-subscriber]
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
|
||||||
# Templates
|
|
||||||
[workspace.dependencies.tera]
|
|
||||||
version = "1.19.1"
|
|
||||||
default-features = false
|
|
||||||
|
|
||||||
# URL manipulation
|
# URL manipulation
|
||||||
[workspace.dependencies.url]
|
[workspace.dependencies.url]
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
|
@@ -46,7 +46,7 @@ impl Options {
|
|||||||
let url_builder =
|
let url_builder =
|
||||||
mas_router::UrlBuilder::new("https://example.com/".parse()?, None, None);
|
mas_router::UrlBuilder::new("https://example.com/".parse()?, None, None);
|
||||||
let templates = templates_from_config(&config, &url_builder).await?;
|
let templates = templates_from_config(&config, &url_builder).await?;
|
||||||
templates.check_render(clock.now(), &mut rng).await?;
|
templates.check_render(clock.now(), &mut rng)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@@ -63,27 +63,18 @@ impl Mailer {
|
|||||||
.reply_to(self.reply_to.clone())
|
.reply_to(self.reply_to.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare_verification_email(
|
fn prepare_verification_email(
|
||||||
&self,
|
&self,
|
||||||
to: Mailbox,
|
to: Mailbox,
|
||||||
context: &EmailVerificationContext,
|
context: &EmailVerificationContext,
|
||||||
) -> Result<Message, Error> {
|
) -> Result<Message, Error> {
|
||||||
let plain = self
|
let plain = self.templates.render_email_verification_txt(context)?;
|
||||||
.templates
|
|
||||||
.render_email_verification_txt(context)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let html = self
|
let html = self.templates.render_email_verification_html(context)?;
|
||||||
.templates
|
|
||||||
.render_email_verification_html(context)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let multipart = MultiPart::alternative_plain_html(plain, html);
|
let multipart = MultiPart::alternative_plain_html(plain, html);
|
||||||
|
|
||||||
let subject = self
|
let subject = self.templates.render_email_verification_subject(context)?;
|
||||||
.templates
|
|
||||||
.render_email_verification_subject(context)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let message = self
|
let message = self
|
||||||
.base_message()
|
.base_message()
|
||||||
@@ -115,7 +106,7 @@ impl Mailer {
|
|||||||
to: Mailbox,
|
to: Mailbox,
|
||||||
context: &EmailVerificationContext,
|
context: &EmailVerificationContext,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let message = self.prepare_verification_email(to, context).await?;
|
let message = self.prepare_verification_email(to, context)?;
|
||||||
self.transport.send(message).await?;
|
self.transport.send(message).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@@ -104,7 +104,7 @@ pub async fn get(
|
|||||||
.with_code("compat_sso_login_expired")
|
.with_code("compat_sso_login_expired")
|
||||||
.with_description("This login session expired.".to_owned());
|
.with_description("This login session expired.".to_owned());
|
||||||
|
|
||||||
let content = templates.render_error(&ctx).await?;
|
let content = templates.render_error(&ctx)?;
|
||||||
return Ok((cookie_jar, Html(content)).into_response());
|
return Ok((cookie_jar, Html(content)).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ pub async fn get(
|
|||||||
.with_session(session)
|
.with_session(session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_sso_login(&ctx).await?;
|
let content = templates.render_sso_login(&ctx)?;
|
||||||
|
|
||||||
Ok((cookie_jar, Html(content)).into_response())
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
}
|
}
|
||||||
@@ -171,7 +171,7 @@ pub async fn post(
|
|||||||
.with_code("compat_sso_login_expired")
|
.with_code("compat_sso_login_expired")
|
||||||
.with_description("This login session expired.".to_owned());
|
.with_description("This login session expired.".to_owned());
|
||||||
|
|
||||||
let content = templates.render_error(&ctx).await?;
|
let content = templates.render_error(&ctx)?;
|
||||||
return Ok((cookie_jar, Html(content)).into_response());
|
return Ok((cookie_jar, Html(content)).into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -409,7 +409,7 @@ where
|
|||||||
// Error responses should have an ErrorContext attached to them
|
// Error responses should have an ErrorContext attached to them
|
||||||
let ext = response.extensions().get::<ErrorContext>();
|
let ext = response.extensions().get::<ErrorContext>();
|
||||||
if let Some(ctx) = ext {
|
if let Some(ctx) = ext {
|
||||||
if let Ok(res) = templates.render_error(ctx).await {
|
if let Ok(res) = templates.render_error(ctx) {
|
||||||
let (mut parts, _original_body) = response.into_parts();
|
let (mut parts, _original_body) = response.into_parts();
|
||||||
parts.headers.remove(CONTENT_TYPE);
|
parts.headers.remove(CONTENT_TYPE);
|
||||||
parts.headers.remove(CONTENT_LENGTH);
|
parts.headers.remove(CONTENT_LENGTH);
|
||||||
@@ -437,7 +437,7 @@ pub async fn fallback(
|
|||||||
let ctx = NotFoundContext::new(&method, version, &uri);
|
let ctx = NotFoundContext::new(&method, version, &uri);
|
||||||
// XXX: this should look at the Accept header and return JSON if requested
|
// XXX: this should look at the Accept header and return JSON if requested
|
||||||
|
|
||||||
let res = templates.render_not_found(&ctx).await?;
|
let res = templates.render_not_found(&ctx)?;
|
||||||
|
|
||||||
Ok((StatusCode::NOT_FOUND, Html(res)))
|
Ok((StatusCode::NOT_FOUND, Html(res)))
|
||||||
}
|
}
|
||||||
|
@@ -164,7 +164,7 @@ impl CallbackDestination {
|
|||||||
params,
|
params,
|
||||||
};
|
};
|
||||||
let ctx = FormPostContext::new(redirect_uri, merged);
|
let ctx = FormPostContext::new(redirect_uri, merged);
|
||||||
let rendered = templates.render_form_post(&ctx).await?;
|
let rendered = templates.render_form_post(&ctx)?;
|
||||||
Ok(Html(rendered).into_response())
|
Ok(Html(rendered).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -162,7 +162,7 @@ pub(crate) async fn get(
|
|||||||
.with_session(session)
|
.with_session(session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_policy_violation(&ctx).await?;
|
let content = templates.render_policy_violation(&ctx)?;
|
||||||
|
|
||||||
Ok((cookie_jar, Html(content)).into_response())
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
}
|
}
|
||||||
|
@@ -420,7 +420,7 @@ pub(crate) async fn get(
|
|||||||
.with_session(user_session)
|
.with_session(user_session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_policy_violation(&ctx).await?;
|
let content = templates.render_policy_violation(&ctx)?;
|
||||||
Html(content).into_response()
|
Html(content).into_response()
|
||||||
}
|
}
|
||||||
Err(GrantCompletionError::RequiresReauth) => {
|
Err(GrantCompletionError::RequiresReauth) => {
|
||||||
|
@@ -125,7 +125,7 @@ pub(crate) async fn get(
|
|||||||
.with_session(session)
|
.with_session(session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_consent(&ctx).await?;
|
let content = templates.render_consent(&ctx)?;
|
||||||
|
|
||||||
Ok((cookie_jar, Html(content)).into_response())
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
} else {
|
} else {
|
||||||
@@ -133,7 +133,7 @@ pub(crate) async fn get(
|
|||||||
.with_session(session)
|
.with_session(session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_policy_violation(&ctx).await?;
|
let content = templates.render_policy_violation(&ctx)?;
|
||||||
|
|
||||||
Ok((cookie_jar, Html(content)).into_response())
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
}
|
}
|
||||||
|
@@ -266,7 +266,7 @@ pub(crate) async fn get(
|
|||||||
.with_session(user_session)
|
.with_session(user_session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
Html(templates.render_upstream_oauth2_link_mismatch(&ctx).await?).into_response()
|
Html(templates.render_upstream_oauth2_link_mismatch(&ctx)?).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
(Some(user_session), None) => {
|
(Some(user_session), None) => {
|
||||||
@@ -275,7 +275,7 @@ pub(crate) async fn get(
|
|||||||
.with_session(user_session)
|
.with_session(user_session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
Html(templates.render_upstream_oauth2_suggest_link(&ctx).await?).into_response()
|
Html(templates.render_upstream_oauth2_suggest_link(&ctx)?).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
(None, Some(user_id)) => {
|
(None, Some(user_id)) => {
|
||||||
@@ -360,7 +360,7 @@ pub(crate) async fn get(
|
|||||||
|
|
||||||
let ctx = ctx.with_csrf(csrf_token.form_value());
|
let ctx = ctx.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
Html(templates.render_upstream_oauth2_do_register(&ctx).await?).into_response()
|
Html(templates.render_upstream_oauth2_do_register(&ctx)?).into_response()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -65,7 +65,7 @@ pub(crate) async fn get(
|
|||||||
.with_session(session)
|
.with_session(session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_account_add_email(&ctx).await?;
|
let content = templates.render_account_add_email(&ctx)?;
|
||||||
|
|
||||||
Ok((cookie_jar, Html(content)).into_response())
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
}
|
}
|
||||||
|
@@ -86,7 +86,7 @@ pub(crate) async fn get(
|
|||||||
.with_session(session)
|
.with_session(session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_account_verify_email(&ctx).await?;
|
let content = templates.render_account_verify_email(&ctx)?;
|
||||||
|
|
||||||
Ok((cookie_jar, Html(content)).into_response())
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
}
|
}
|
||||||
|
@@ -88,7 +88,7 @@ async fn render(
|
|||||||
.with_session(session)
|
.with_session(session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_account_password(&ctx).await?;
|
let content = templates.render_account_password(&ctx)?;
|
||||||
|
|
||||||
Ok((cookie_jar, Html(content)).into_response())
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
}
|
}
|
||||||
|
@@ -50,7 +50,7 @@ pub async fn get(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let ctx = AppContext::default();
|
let ctx = AppContext::default();
|
||||||
let content = templates.render_app(&ctx).await?;
|
let content = templates.render_app(&ctx)?;
|
||||||
|
|
||||||
Ok((cookie_jar, Html(content)).into_response())
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
}
|
}
|
||||||
|
@@ -47,7 +47,7 @@ pub async fn get(
|
|||||||
.maybe_with_session(session)
|
.maybe_with_session(session)
|
||||||
.with_csrf(csrf_token.form_value());
|
.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_index(&ctx).await?;
|
let content = templates.render_index(&ctx)?;
|
||||||
|
|
||||||
tracing::info!("rendered index page");
|
tracing::info!("rendered index page");
|
||||||
|
|
||||||
|
@@ -289,7 +289,7 @@ async fn render(
|
|||||||
};
|
};
|
||||||
let ctx = ctx.with_csrf(csrf_token.form_value());
|
let ctx = ctx.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_login(&ctx).await?;
|
let content = templates.render_login(&ctx)?;
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -81,7 +81,7 @@ pub(crate) async fn get(
|
|||||||
};
|
};
|
||||||
let ctx = ctx.with_session(session).with_csrf(csrf_token.form_value());
|
let ctx = ctx.with_session(session).with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_reauth(&ctx).await?;
|
let content = templates.render_reauth(&ctx)?;
|
||||||
|
|
||||||
Ok((cookie_jar, Html(content)).into_response())
|
Ok((cookie_jar, Html(content)).into_response())
|
||||||
}
|
}
|
||||||
|
@@ -252,7 +252,7 @@ async fn render(
|
|||||||
};
|
};
|
||||||
let ctx = ctx.with_csrf(csrf_token.form_value());
|
let ctx = ctx.with_csrf(csrf_token.form_value());
|
||||||
|
|
||||||
let content = templates.render_register(&ctx).await?;
|
let content = templates.render_register(&ctx)?;
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,7 +10,7 @@ repository.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
camino.workspace = true
|
camino.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
minijinja = { version = "1.0.8", features = ["unstable_machinery"] }
|
minijinja = { workspace = true, features = ["unstable_machinery"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tera.workspace = true
|
tera.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
@@ -8,13 +8,15 @@ homepage.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
arc-swap = "1.6.0"
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tokio = { version = "1.32.0", features = ["macros", "rt", "fs"] }
|
tokio = { version = "1.32.0", features = ["macros", "rt", "fs"] }
|
||||||
|
walkdir = "2.4.0"
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
|
|
||||||
tera.workspace = true
|
minijinja = { workspace = true, features = ["loader", "json"] }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
serde_urlencoded = "0.7.1"
|
serde_urlencoded = "0.7.1"
|
||||||
|
@@ -12,6 +12,9 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
// This is needed to make the Environment::add* functions work
|
||||||
|
#![allow(clippy::needless_pass_by_value)]
|
||||||
|
|
||||||
//! Additional functions, tests and filters used in templates
|
//! Additional functions, tests and filters used in templates
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
@@ -22,78 +25,80 @@ use std::{
|
|||||||
use camino::Utf8Path;
|
use camino::Utf8Path;
|
||||||
use mas_router::UrlBuilder;
|
use mas_router::UrlBuilder;
|
||||||
use mas_spa::ViteManifest;
|
use mas_spa::ViteManifest;
|
||||||
use tera::{helpers::tests::number_args_allowed, Tera, Value};
|
use minijinja::{
|
||||||
|
value::{from_args, Kwargs, Object, SeqObject, ViaDeserialize},
|
||||||
|
Error, ErrorKind, State, Value,
|
||||||
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub fn register(tera: &mut Tera, url_builder: UrlBuilder, vite_manifest: ViteManifest) {
|
pub fn register(
|
||||||
tera.register_tester("empty", self::tester_empty);
|
env: &mut minijinja::Environment,
|
||||||
tera.register_filter("to_params", filter_to_params);
|
url_builder: UrlBuilder,
|
||||||
tera.register_filter("safe_get", filter_safe_get);
|
vite_manifest: ViteManifest,
|
||||||
tera.register_filter("simplify_url", filter_simplify_url);
|
) {
|
||||||
tera.register_function("add_params_to_url", function_add_params_to_url);
|
env.add_test("empty", self::tester_empty);
|
||||||
tera.register_function("merge", function_merge);
|
env.add_test("starting_with", tester_starting_with);
|
||||||
tera.register_function("dict", function_dict);
|
env.add_filter("to_params", filter_to_params);
|
||||||
tera.register_function(
|
env.add_filter("simplify_url", filter_simplify_url);
|
||||||
|
env.add_filter("add_slashes", filter_add_slashes);
|
||||||
|
env.add_filter("split", filter_split);
|
||||||
|
env.add_function("add_params_to_url", function_add_params_to_url);
|
||||||
|
//tera.register_function("merge", function_merge);
|
||||||
|
env.add_global(
|
||||||
"include_asset",
|
"include_asset",
|
||||||
IncludeAsset {
|
Value::from_object(IncludeAsset {
|
||||||
url_builder,
|
url_builder,
|
||||||
vite_manifest,
|
vite_manifest,
|
||||||
},
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tester_empty(value: Option<&Value>, params: &[Value]) -> Result<bool, tera::Error> {
|
fn tester_empty(seq: &dyn SeqObject) -> bool {
|
||||||
number_args_allowed("empty", 0, params.len())?;
|
seq.item_count() == 0
|
||||||
|
|
||||||
match value.and_then(Value::as_array).map(|v| &v[..]) {
|
|
||||||
Some(&[]) | None => Ok(true),
|
|
||||||
Some(_) => Ok(false),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter_to_params(params: &Value, kv: &HashMap<String, Value>) -> Result<Value, tera::Error> {
|
fn tester_starting_with(value: &str, prefix: &str) -> bool {
|
||||||
let prefix = kv.get("prefix").and_then(Value::as_str).unwrap_or("");
|
value.starts_with(prefix)
|
||||||
let params = serde_urlencoded::to_string(params)
|
}
|
||||||
.map_err(|e| tera::Error::chain(e, "Could not serialize parameters"))?;
|
|
||||||
|
fn filter_split(value: &str, separator: &str) -> Vec<String> {
|
||||||
|
value
|
||||||
|
.split(separator)
|
||||||
|
.map(std::borrow::ToOwned::to_owned)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_add_slashes(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.replace('\\', "\\\\")
|
||||||
|
.replace('\"', "\\\"")
|
||||||
|
.replace('\'', "\\\'")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_to_params(params: &Value, kwargs: Kwargs) -> Result<String, Error> {
|
||||||
|
let params = serde_urlencoded::to_string(params).map_err(|e| {
|
||||||
|
Error::new(
|
||||||
|
ErrorKind::InvalidOperation,
|
||||||
|
"Could not serialize parameters",
|
||||||
|
)
|
||||||
|
.with_source(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let prefix = kwargs.get("prefix").unwrap_or("");
|
||||||
|
kwargs.assert_all_used()?;
|
||||||
|
|
||||||
if params.is_empty() {
|
if params.is_empty() {
|
||||||
Ok(Value::String(String::new()))
|
Ok(String::new())
|
||||||
} else {
|
} else {
|
||||||
Ok(Value::String(format!("{prefix}{params}")))
|
Ok(format!("{prefix}{params}"))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Alternative to `get` which does not crash on `None` and defaults to `None`
|
|
||||||
pub fn filter_safe_get(value: &Value, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
|
|
||||||
let default = args.get("default").unwrap_or(&Value::Null);
|
|
||||||
let key = args
|
|
||||||
.get("key")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.ok_or_else(|| tera::Error::msg("Invalid parameter `uri`"))?;
|
|
||||||
|
|
||||||
match value.as_object() {
|
|
||||||
Some(o) => match o.get(key) {
|
|
||||||
Some(val) => Ok(val.clone()),
|
|
||||||
// If the value is not present, allow for an optional default value
|
|
||||||
None => Ok(default.clone()),
|
|
||||||
},
|
|
||||||
None => Ok(default.clone()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter which simplifies a URL to its domain name for HTTP(S) URLs
|
/// Filter which simplifies a URL to its domain name for HTTP(S) URLs
|
||||||
fn filter_simplify_url(value: &Value, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
|
fn filter_simplify_url(url: &str) -> String {
|
||||||
let url = value
|
|
||||||
.as_str()
|
|
||||||
.ok_or_else(|| tera::Error::msg("Invalid input for `simplify_url` filter"))?;
|
|
||||||
|
|
||||||
if !args.is_empty() {
|
|
||||||
return Err(tera::Error::msg("`simplify_url` filter takes no arguments"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do nothing if the URL is not valid
|
// Do nothing if the URL is not valid
|
||||||
let Ok(mut url) = Url::from_str(url) else {
|
let Ok(mut url) = Url::from_str(url) else {
|
||||||
return Ok(Value::String(url.to_owned()));
|
return url.to_owned();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Always at least remove the query parameters and fragment
|
// Always at least remove the query parameters and fragment
|
||||||
@@ -102,15 +107,15 @@ fn filter_simplify_url(value: &Value, args: &HashMap<String, Value>) -> Result<V
|
|||||||
|
|
||||||
// Do nothing else for non-HTTPS URLs
|
// Do nothing else for non-HTTPS URLs
|
||||||
if url.scheme() != "https" {
|
if url.scheme() != "https" {
|
||||||
return Ok(Value::String(url.to_string()));
|
return url.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only return the domain name
|
// Only return the domain name
|
||||||
let Some(domain) = url.domain() else {
|
let Some(domain) = url.domain() else {
|
||||||
return Ok(Value::String(url.to_string()));
|
return url.to_string();
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::String(domain.to_owned()))
|
domain.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ParamsWhere {
|
enum ParamsWhere {
|
||||||
@@ -118,29 +123,25 @@ enum ParamsWhere {
|
|||||||
Query,
|
Query,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn function_add_params_to_url(params: &HashMap<String, Value>) -> Result<Value, tera::Error> {
|
fn function_add_params_to_url(
|
||||||
|
uri: ViaDeserialize<Url>,
|
||||||
|
mode: &str,
|
||||||
|
params: ViaDeserialize<HashMap<String, Value>>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
use ParamsWhere::{Fragment, Query};
|
use ParamsWhere::{Fragment, Query};
|
||||||
|
|
||||||
// First, get the `uri`, `mode` and `params` parameters
|
|
||||||
let uri = params
|
|
||||||
.get("uri")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.ok_or_else(|| tera::Error::msg("Invalid parameter `uri`"))?;
|
|
||||||
let uri = Url::from_str(uri).map_err(|e| tera::Error::chain(uri, e))?;
|
|
||||||
let mode = params
|
|
||||||
.get("mode")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.ok_or_else(|| tera::Error::msg("Invalid parameter `mode`"))?;
|
|
||||||
let mode = match mode {
|
let mode = match mode {
|
||||||
"fragment" => Fragment,
|
"fragment" => Fragment,
|
||||||
"query" => Query,
|
"query" => Query,
|
||||||
_ => return Err(tera::Error::msg("Invalid mode")),
|
_ => {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidOperation,
|
||||||
|
"Invalid `mode` parameter",
|
||||||
|
))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let params = params
|
|
||||||
.get("params")
|
|
||||||
.and_then(Value::as_object)
|
|
||||||
.ok_or_else(|| tera::Error::msg("Invalid parameter `params`"))?;
|
|
||||||
|
|
||||||
|
// First, get the `uri`, `mode` and `params` parameters
|
||||||
// Get the relevant part of the URI and parse for existing parameters
|
// Get the relevant part of the URI and parse for existing parameters
|
||||||
let existing = match mode {
|
let existing = match mode {
|
||||||
Fragment => uri.fragment(),
|
Fragment => uri.fragment(),
|
||||||
@@ -149,15 +150,26 @@ fn function_add_params_to_url(params: &HashMap<String, Value>) -> Result<Value,
|
|||||||
let existing: HashMap<String, Value> = existing
|
let existing: HashMap<String, Value> = existing
|
||||||
.map(serde_urlencoded::from_str)
|
.map(serde_urlencoded::from_str)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(|e| tera::Error::chain(e, "Could not parse existing `uri` parameters"))?
|
.map_err(|e| {
|
||||||
|
Error::new(
|
||||||
|
ErrorKind::InvalidOperation,
|
||||||
|
"Could not parse existing `uri` parameters",
|
||||||
|
)
|
||||||
|
.with_source(e)
|
||||||
|
})?
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Merge the exising and the additional parameters together
|
// Merge the exising and the additional parameters together
|
||||||
let params: HashMap<&String, &Value> = params.iter().chain(existing.iter()).collect();
|
let params: HashMap<&String, &Value> = params.iter().chain(existing.iter()).collect();
|
||||||
|
|
||||||
// Transform them back to urlencoded
|
// Transform them back to urlencoded
|
||||||
let params = serde_urlencoded::to_string(params)
|
let params = serde_urlencoded::to_string(params).map_err(|e| {
|
||||||
.map_err(|e| tera::Error::chain(e, "Could not serialize back parameters"))?;
|
Error::new(
|
||||||
|
ErrorKind::InvalidOperation,
|
||||||
|
"Could not serialize back parameters",
|
||||||
|
)
|
||||||
|
.with_source(e)
|
||||||
|
})?;
|
||||||
|
|
||||||
let uri = {
|
let uri = {
|
||||||
let mut uri = uri;
|
let mut uri = uri;
|
||||||
@@ -168,9 +180,10 @@ fn function_add_params_to_url(params: &HashMap<String, Value>) -> Result<Value,
|
|||||||
uri
|
uri
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Value::String(uri.to_string()))
|
Ok(uri.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
fn function_merge(params: &HashMap<String, Value>) -> Result<Value, tera::Error> {
|
fn function_merge(params: &HashMap<String, Value>) -> Result<Value, tera::Error> {
|
||||||
let mut ret = serde_json::Map::new();
|
let mut ret = serde_json::Map::new();
|
||||||
for (k, v) in params {
|
for (k, v) in params {
|
||||||
@@ -182,50 +195,41 @@ fn function_merge(params: &HashMap<String, Value>) -> Result<Value, tera::Error>
|
|||||||
|
|
||||||
Ok(Value::Object(ret))
|
Ok(Value::Object(ret))
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
#[derive(Debug)]
|
||||||
fn function_dict(params: &HashMap<String, Value>) -> Result<Value, tera::Error> {
|
|
||||||
let ret = params.clone().into_iter().collect();
|
|
||||||
Ok(Value::Object(ret))
|
|
||||||
}
|
|
||||||
|
|
||||||
struct IncludeAsset {
|
struct IncludeAsset {
|
||||||
url_builder: UrlBuilder,
|
url_builder: UrlBuilder,
|
||||||
vite_manifest: ViteManifest,
|
vite_manifest: ViteManifest,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl tera::Function for IncludeAsset {
|
impl std::fmt::Display for IncludeAsset {
|
||||||
fn call(&self, args: &HashMap<String, Value>) -> tera::Result<Value> {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let path = args.get("path").ok_or(tera::Error::msg(
|
f.write_str("include_asset")
|
||||||
"Function `include_asset` was missing parameter `path`",
|
}
|
||||||
))?;
|
}
|
||||||
|
|
||||||
let preload = args
|
impl Object for IncludeAsset {
|
||||||
.get("preload")
|
fn call(&self, _state: &State, args: &[Value]) -> Result<Value, Error> {
|
||||||
.and_then(Value::as_bool)
|
let (path, kwargs): (&str, Kwargs) = from_args(args)?;
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
let path: &Utf8Path = path
|
let preload = kwargs.get("preload").unwrap_or(false);
|
||||||
.as_str()
|
kwargs.assert_all_used()?;
|
||||||
.ok_or_else(|| {
|
|
||||||
tera::Error::msg(
|
|
||||||
"Function `include_asset` received an incorrect type for arg `path`",
|
|
||||||
)
|
|
||||||
})?
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let assets = self.vite_manifest.assets_for(path).map_err(|e| {
|
let path: &Utf8Path = path.into();
|
||||||
tera::Error::chain(
|
|
||||||
|
let assets = self.vite_manifest.assets_for(path).map_err(|_e| {
|
||||||
|
Error::new(
|
||||||
|
ErrorKind::InvalidOperation,
|
||||||
"Invalid assets manifest while calling function `include_asset`",
|
"Invalid assets manifest while calling function `include_asset`",
|
||||||
e.to_string(),
|
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let preloads = if preload {
|
let preloads = if preload {
|
||||||
self.vite_manifest.preload_for(path).map_err(|e| {
|
self.vite_manifest.preload_for(path).map_err(|_e| {
|
||||||
tera::Error::chain(
|
Error::new(
|
||||||
|
ErrorKind::InvalidOperation,
|
||||||
"Invalid assets manifest while calling function `include_asset`",
|
"Invalid assets manifest while calling function `include_asset`",
|
||||||
e.to_string(),
|
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
} else {
|
} else {
|
||||||
@@ -242,10 +246,6 @@ impl tera::Function for IncludeAsset {
|
|||||||
)
|
)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Value::String(tags.join("\n")))
|
Ok(Value::from_safe_string(tags.join("\n")))
|
||||||
}
|
|
||||||
|
|
||||||
fn is_safe(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,19 +24,19 @@
|
|||||||
|
|
||||||
//! Templates rendering
|
//! Templates rendering
|
||||||
|
|
||||||
use std::{collections::HashSet, string::ToString, sync::Arc};
|
use std::{collections::HashSet, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
|
use arc_swap::ArcSwap;
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
use mas_router::UrlBuilder;
|
use mas_router::UrlBuilder;
|
||||||
use mas_spa::ViteManifest;
|
use mas_spa::ViteManifest;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
pub use tera::escape_html;
|
|
||||||
use tera::{Context, Error as TeraError, Tera};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::{sync::RwLock, task::JoinError};
|
use tokio::task::JoinError;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
use walkdir::DirEntry;
|
||||||
|
|
||||||
mod context;
|
mod context;
|
||||||
mod forms;
|
mod forms;
|
||||||
@@ -57,10 +57,11 @@ pub use self::{
|
|||||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Wrapper around [`tera::Tera`] helping rendering the various templates
|
/// Wrapper around [`minijinja::Environment`] helping rendering the various
|
||||||
|
/// templates
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Templates {
|
pub struct Templates {
|
||||||
tera: Arc<RwLock<Tera>>,
|
environment: Arc<ArcSwap<minijinja::Environment<'static>>>,
|
||||||
url_builder: UrlBuilder,
|
url_builder: UrlBuilder,
|
||||||
vite_manifest_path: Utf8PathBuf,
|
vite_manifest_path: Utf8PathBuf,
|
||||||
path: Utf8PathBuf,
|
path: Utf8PathBuf,
|
||||||
@@ -81,9 +82,25 @@ pub enum TemplateLoadingError {
|
|||||||
#[error("invalid assets manifest")]
|
#[error("invalid assets manifest")]
|
||||||
ViteManifest(#[from] serde_json::Error),
|
ViteManifest(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
/// Failed to traverse the filesystem
|
||||||
|
#[error("failed to traverse the filesystem")]
|
||||||
|
WalkDir(#[from] walkdir::Error),
|
||||||
|
|
||||||
|
/// Encountered non-UTF-8 path
|
||||||
|
#[error("encountered non-UTF-8 path")]
|
||||||
|
NonUtf8Path(#[from] camino::FromPathError),
|
||||||
|
|
||||||
|
/// Encountered non-UTF-8 path
|
||||||
|
#[error("encountered non-UTF-8 path")]
|
||||||
|
NonUtf8PathBuf(#[from] camino::FromPathBufError),
|
||||||
|
|
||||||
|
/// Encountered invalid path
|
||||||
|
#[error("encountered invalid path")]
|
||||||
|
InvalidPath(#[from] std::path::StripPrefixError),
|
||||||
|
|
||||||
/// Some templates failed to compile
|
/// Some templates failed to compile
|
||||||
#[error("could not load and compile some templates")]
|
#[error("could not load and compile some templates")]
|
||||||
Compile(#[from] TeraError),
|
Compile(#[from] minijinja::Error),
|
||||||
|
|
||||||
/// Could not join blocking task
|
/// Could not join blocking task
|
||||||
#[error("error from async runtime")]
|
#[error("error from async runtime")]
|
||||||
@@ -99,6 +116,13 @@ pub enum TemplateLoadingError {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_hidden(entry: &DirEntry) -> bool {
|
||||||
|
entry
|
||||||
|
.file_name()
|
||||||
|
.to_str()
|
||||||
|
.is_some_and(|s| s.starts_with('.'))
|
||||||
|
}
|
||||||
|
|
||||||
impl Templates {
|
impl Templates {
|
||||||
/// Load the templates from the given config
|
/// Load the templates from the given config
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
@@ -112,9 +136,9 @@ impl Templates {
|
|||||||
url_builder: UrlBuilder,
|
url_builder: UrlBuilder,
|
||||||
vite_manifest_path: Utf8PathBuf,
|
vite_manifest_path: Utf8PathBuf,
|
||||||
) -> Result<Self, TemplateLoadingError> {
|
) -> Result<Self, TemplateLoadingError> {
|
||||||
let tera = Self::load_(&path, url_builder.clone(), &vite_manifest_path).await?;
|
let environment = Self::load_(&path, url_builder.clone(), &vite_manifest_path).await?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
tera: Arc::new(RwLock::new(tera)),
|
environment: Arc::new(ArcSwap::from_pointee(environment)),
|
||||||
path,
|
path,
|
||||||
url_builder,
|
url_builder,
|
||||||
vite_manifest_path,
|
vite_manifest_path,
|
||||||
@@ -125,7 +149,7 @@ impl Templates {
|
|||||||
path: &Utf8Path,
|
path: &Utf8Path,
|
||||||
url_builder: UrlBuilder,
|
url_builder: UrlBuilder,
|
||||||
vite_manifest_path: &Utf8Path,
|
vite_manifest_path: &Utf8Path,
|
||||||
) -> Result<Tera, TemplateLoadingError> {
|
) -> Result<minijinja::Environment<'static>, TemplateLoadingError> {
|
||||||
let path = path.to_owned();
|
let path = path.to_owned();
|
||||||
let span = tracing::Span::current();
|
let span = tracing::Span::current();
|
||||||
|
|
||||||
@@ -138,30 +162,48 @@ impl Templates {
|
|||||||
let vite_manifest: ViteManifest =
|
let vite_manifest: ViteManifest =
|
||||||
serde_json::from_slice(&vite_manifest).map_err(TemplateLoadingError::ViteManifest)?;
|
serde_json::from_slice(&vite_manifest).map_err(TemplateLoadingError::ViteManifest)?;
|
||||||
|
|
||||||
// This uses blocking I/Os, do that in a blocking task
|
let (loaded, mut env) = tokio::task::spawn_blocking(move || {
|
||||||
let mut tera = tokio::task::spawn_blocking(move || {
|
|
||||||
span.in_scope(move || {
|
span.in_scope(move || {
|
||||||
let path = path.canonicalize_utf8()?;
|
let mut loaded: HashSet<_> = HashSet::new();
|
||||||
let path = format!("{path}/**/*.{{html,txt,subject}}");
|
let mut env = minijinja::Environment::new();
|
||||||
|
let root = path.canonicalize_utf8()?;
|
||||||
|
info!(%root, "Loading templates from filesystem");
|
||||||
|
for entry in walkdir::WalkDir::new(&root)
|
||||||
|
.min_depth(1)
|
||||||
|
.into_iter()
|
||||||
|
.filter_entry(|e| !is_hidden(e))
|
||||||
|
{
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type().is_file() {
|
||||||
|
let path = Utf8PathBuf::try_from(entry.into_path())?;
|
||||||
|
let Some(ext) = path.extension() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
info!(%path, "Loading templates from filesystem");
|
if ext == "html" || ext == "txt" || ext == "subject" {
|
||||||
Tera::new(&path)
|
let relative = path.strip_prefix(&root)?;
|
||||||
|
debug!(%relative, "Registering template");
|
||||||
|
let template = std::fs::read_to_string(&path)?;
|
||||||
|
env.add_template_owned(relative.as_str().to_owned(), template)?;
|
||||||
|
loaded.insert(relative.as_str().to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<_, TemplateLoadingError>((loaded, env))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
self::functions::register(&mut tera, url_builder, vite_manifest);
|
self::functions::register(&mut env, url_builder, vite_manifest);
|
||||||
|
|
||||||
let loaded: HashSet<_> = tera.get_template_names().collect();
|
let needed: HashSet<_> = TEMPLATES.into_iter().map(ToOwned::to_owned).collect();
|
||||||
let needed: HashSet<_> = TEMPLATES.into_iter().collect();
|
|
||||||
debug!(?loaded, ?needed, "Templates loaded");
|
debug!(?loaded, ?needed, "Templates loaded");
|
||||||
let missing: HashSet<_> = needed.difference(&loaded).collect();
|
let missing: HashSet<_> = needed.difference(&loaded).cloned().collect();
|
||||||
|
|
||||||
if missing.is_empty() {
|
if missing.is_empty() {
|
||||||
Ok(tera)
|
Ok(env)
|
||||||
} else {
|
} else {
|
||||||
let missing = missing.into_iter().map(ToString::to_string).collect();
|
|
||||||
let loaded = loaded.into_iter().map(ToString::to_string).collect();
|
|
||||||
Err(TemplateLoadingError::MissingTemplates { missing, loaded })
|
Err(TemplateLoadingError::MissingTemplates { missing, loaded })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,8 +216,7 @@ impl Templates {
|
|||||||
err,
|
err,
|
||||||
)]
|
)]
|
||||||
pub async fn reload(&self) -> Result<(), TemplateLoadingError> {
|
pub async fn reload(&self) -> Result<(), TemplateLoadingError> {
|
||||||
// Prepare the new Tera instance
|
let new_minijinja = Self::load_(
|
||||||
let new_tera = Self::load_(
|
|
||||||
&self.path,
|
&self.path,
|
||||||
self.url_builder.clone(),
|
self.url_builder.clone(),
|
||||||
&self.vite_manifest_path,
|
&self.vite_manifest_path,
|
||||||
@@ -183,7 +224,7 @@ impl Templates {
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Swap it
|
// Swap it
|
||||||
*self.tera.write().await = new_tera;
|
self.environment.store(Arc::new(new_minijinja));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -192,15 +233,15 @@ impl Templates {
|
|||||||
/// Failed to render a template
|
/// Failed to render a template
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum TemplateError {
|
pub enum TemplateError {
|
||||||
/// Failed to prepare the context used by this template
|
/// Missing template
|
||||||
#[error("could not prepare context for template {template:?}")]
|
#[error("missing template {template:?}")]
|
||||||
Context {
|
Missing {
|
||||||
/// The name of the template being rendered
|
/// The name of the template being rendered
|
||||||
template: &'static str,
|
template: &'static str,
|
||||||
|
|
||||||
/// The underlying error
|
/// The underlying error
|
||||||
#[source]
|
#[source]
|
||||||
source: TeraError,
|
source: minijinja::Error,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Failed to render the template
|
/// Failed to render the template
|
||||||
@@ -211,7 +252,7 @@ pub enum TemplateError {
|
|||||||
|
|
||||||
/// The underlying error
|
/// The underlying error
|
||||||
#[source]
|
#[source]
|
||||||
source: TeraError,
|
source: minijinja::Error,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,31 +321,31 @@ register_templates! {
|
|||||||
impl Templates {
|
impl Templates {
|
||||||
/// Render all templates with the generated samples to check if they render
|
/// Render all templates with the generated samples to check if they render
|
||||||
/// properly
|
/// properly
|
||||||
pub async fn check_render(
|
pub fn check_render(
|
||||||
&self,
|
&self,
|
||||||
now: chrono::DateTime<chrono::Utc>,
|
now: chrono::DateTime<chrono::Utc>,
|
||||||
rng: &mut impl Rng,
|
rng: &mut impl Rng,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
check::render_not_found(self, now, rng).await?;
|
check::render_not_found(self, now, rng)?;
|
||||||
check::render_app(self, now, rng).await?;
|
check::render_app(self, now, rng)?;
|
||||||
check::render_login(self, now, rng).await?;
|
check::render_login(self, now, rng)?;
|
||||||
check::render_register(self, now, rng).await?;
|
check::render_register(self, now, rng)?;
|
||||||
check::render_consent(self, now, rng).await?;
|
check::render_consent(self, now, rng)?;
|
||||||
check::render_policy_violation(self, now, rng).await?;
|
check::render_policy_violation(self, now, rng)?;
|
||||||
check::render_sso_login(self, now, rng).await?;
|
check::render_sso_login(self, now, rng)?;
|
||||||
check::render_index(self, now, rng).await?;
|
check::render_index(self, now, rng)?;
|
||||||
check::render_account_password(self, now, rng).await?;
|
check::render_account_password(self, now, rng)?;
|
||||||
check::render_account_add_email(self, now, rng).await?;
|
check::render_account_add_email(self, now, rng)?;
|
||||||
check::render_account_verify_email(self, now, rng).await?;
|
check::render_account_verify_email(self, now, rng)?;
|
||||||
check::render_reauth(self, now, rng).await?;
|
check::render_reauth(self, now, rng)?;
|
||||||
check::render_form_post::<EmptyContext>(self, now, rng).await?;
|
check::render_form_post::<EmptyContext>(self, now, rng)?;
|
||||||
check::render_error(self, now, rng).await?;
|
check::render_error(self, now, rng)?;
|
||||||
check::render_email_verification_txt(self, now, rng).await?;
|
check::render_email_verification_txt(self, now, rng)?;
|
||||||
check::render_email_verification_html(self, now, rng).await?;
|
check::render_email_verification_html(self, now, rng)?;
|
||||||
check::render_email_verification_subject(self, now, rng).await?;
|
check::render_email_verification_subject(self, now, rng)?;
|
||||||
check::render_upstream_oauth2_link_mismatch(self, now, rng).await?;
|
check::render_upstream_oauth2_link_mismatch(self, now, rng)?;
|
||||||
check::render_upstream_oauth2_suggest_link(self, now, rng).await?;
|
check::render_upstream_oauth2_suggest_link(self, now, rng)?;
|
||||||
check::render_upstream_oauth2_do_register(self, now, rng).await?;
|
check::render_upstream_oauth2_do_register(self, now, rng)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,6 +368,6 @@ mod tests {
|
|||||||
let templates = Templates::load(path, url_builder, vite_manifest_path)
|
let templates = Templates::load(path, url_builder, vite_manifest_path)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
templates.check_render(now, &mut rng).await.unwrap();
|
templates.check_render(now, &mut rng).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -54,14 +54,16 @@ macro_rules! register_templates {
|
|||||||
impl Templates {
|
impl Templates {
|
||||||
$(
|
$(
|
||||||
$(#[$attr])?
|
$(#[$attr])?
|
||||||
pub async fn $name
|
pub fn $name
|
||||||
$(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
|
$(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
|
||||||
(&self, context: &$param)
|
(&self, context: &$param)
|
||||||
-> Result<String, TemplateError> {
|
-> Result<String, TemplateError> {
|
||||||
let ctx = Context::from_serialize(context)
|
let ctx = ::minijinja::value::Value::from_serializable(context);
|
||||||
.map_err(|source| TemplateError::Context { template: $template, source })?;
|
|
||||||
|
|
||||||
self.tera.read().await.render($template, &ctx)
|
let env = self.environment.load();
|
||||||
|
let tmpl = env.get_template($template)
|
||||||
|
.map_err(|source| TemplateError::Missing { template: $template, source })?;
|
||||||
|
tmpl.render(ctx)
|
||||||
.map_err(|source| TemplateError::Render { template: $template, source })
|
.map_err(|source| TemplateError::Render { template: $template, source })
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
@@ -73,7 +75,7 @@ macro_rules! register_templates {
|
|||||||
|
|
||||||
$(
|
$(
|
||||||
#[doc = concat!("Render the `", $template, "` template with sample contexts")]
|
#[doc = concat!("Render the `", $template, "` template with sample contexts")]
|
||||||
pub async fn $name
|
pub fn $name
|
||||||
$(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)?
|
$(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)?
|
||||||
(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut impl rand::Rng)
|
(templates: &Templates, now: chrono::DateTime<chrono::Utc>, rng: &mut impl rand::Rng)
|
||||||
-> anyhow::Result<()> {
|
-> anyhow::Result<()> {
|
||||||
@@ -84,7 +86,6 @@ macro_rules! register_templates {
|
|||||||
let context = serde_json::to_value(&sample)?;
|
let context = serde_json::to_value(&sample)?;
|
||||||
::tracing::info!(name, %context, "Rendering template");
|
::tracing::info!(name, %context, "Rendering template");
|
||||||
templates. $name (&sample)
|
templates. $name (&sample)
|
||||||
.await
|
|
||||||
.with_context(|| format!("Failed to render template {:?} with context {}", name, context))?;
|
.with_context(|| format!("Failed to render template {:?} with context {}", name, context))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,7 +23,7 @@ limitations under the License.
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>matrix-authentication-service</title>
|
<title>matrix-authentication-service</title>
|
||||||
<script>
|
<script>
|
||||||
window.APP_CONFIG = JSON.parse("{{ app_config | json_encode | addslashes | safe }}");
|
window.APP_CONFIG = JSON.parse("{{ app_config | tojson | add_slashes | safe }}");
|
||||||
(function () {
|
(function () {
|
||||||
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
function handleChange(list) {
|
function handleChange(list) {
|
||||||
@@ -38,7 +38,7 @@ limitations under the License.
|
|||||||
handleChange(query);
|
handleChange(query);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
{{ include_asset(path='src/main.tsx', preload=true) | indent(prefix=" ") | safe }}
|
{{ include_asset('src/main.tsx', preload=true) | indent(4) | safe }}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@@ -29,7 +29,7 @@ limitations under the License.
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{% block title %}matrix-authentication-service{% endblock title %}</title>
|
<title>{% block title %}matrix-authentication-service{% endblock title %}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
{{ include_asset(path='src/templates.css', preload=true) | indent(prefix=" ") | safe }}
|
{{ include_asset('src/templates.css', preload=true) | indent(4) | safe }}
|
||||||
</head>
|
</head>
|
||||||
<body class="flex flex-col min-h-screen">
|
<body class="flex flex-col min-h-screen">
|
||||||
{% block content %}{% endblock content %}
|
{% block content %}{% endblock content %}
|
||||||
|
@@ -25,13 +25,13 @@ limitations under the License.
|
|||||||
|
|
||||||
{% if mode == "form_post" %}
|
{% if mode == "form_post" %}
|
||||||
<form method="post" action="{{ uri }}">
|
<form method="post" action="{{ uri }}">
|
||||||
{% for key, value in params %}
|
{% for key, value in params|items %}
|
||||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<button class="{{ class }}" data-kind="{{ kind }}" data-size="lg" type="submit">{{ text }}</button>
|
<button class="{{ class }}" data-kind="{{ kind }}" data-size="lg" type="submit">{{ text }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% elif mode == "fragment" or mode == "query" %}
|
{% elif mode == "fragment" or mode == "query" %}
|
||||||
<a class="{{ class }}" data-kind="{{ kind }}" data-size="lg" href="{{ add_params_to_url(uri=uri, mode=mode, params=params) }}">{{ text }}</a>
|
<a class="{{ class }}" data-kind="{{ kind }}" data-size="lg" href="{{ add_params_to_url(uri, mode, params) }}">{{ text }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ throw(message="Invalid mode") }}
|
{{ throw(message="Invalid mode") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -16,10 +16,10 @@ limitations under the License.
|
|||||||
|
|
||||||
{% macro input(label, name, type="text", form_state=false, autocomplete=false, class="", inputmode="text", autocorrect=false, autocapitalize=false, disabled=false, required=false) %}
|
{% macro input(label, name, type="text", form_state=false, autocomplete=false, class="", inputmode="text", autocorrect=false, autocapitalize=false, disabled=false, required=false) %}
|
||||||
{% if not form_state %}
|
{% if not form_state %}
|
||||||
{% set form_state = dict(errors=[], fields=dict()) %}
|
{% set form_state = {"errors": [], "fields": {}} %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set state = form_state.fields[name] | default(value=dict(errors=[], value="")) %}
|
{% set state = form_state.fields[name] | default({"errors": [], "value": ""}) %}
|
||||||
|
|
||||||
<div class="flex flex-col cpd-field {{ class }}">
|
<div class="flex flex-col cpd-field {{ class }}">
|
||||||
<div class="cpd-label"{% if state.errors is not empty %} data-invalid{% endif %}>{{ label }}</div>
|
<div class="cpd-label"{% if state.errors is not empty %} data-invalid{% endif %}>{{ label }}</div>
|
||||||
@@ -53,4 +53,4 @@ limitations under the License.
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endmacro input %}
|
{% endmacro %}
|
||||||
|
@@ -14,14 +14,12 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
#}
|
#}
|
||||||
|
|
||||||
{% macro button(text, csrf_token, as_link=false, post_logout_action=false) %}
|
{% macro button(text, csrf_token, as_link=false, post_logout_action={}) %}
|
||||||
<form method="POST" action="/logout" class="inline">
|
<form method="POST" action="/logout" class="inline">
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
{% if post_logout_action %}
|
{% for key, value in post_logout_action|items %}
|
||||||
{% for key, value in post_logout_action %}
|
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
<button class="{% if as_link %}cpd-link{% else %}cpd-button{% endif %}" data-kind="critical" type="submit">{{ text }}</button>
|
<button class="{% if as_link %}cpd-link{% else %}cpd-button{% endif %}" data-kind="critical" type="submit">{{ text }}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@@ -24,12 +24,12 @@ limitations under the License.
|
|||||||
Signed in as <span class="font-semibold">{{ current_session.user.username }}</span>.
|
Signed in as <span class="font-semibold">{{ current_session.user.username }}</span>.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ button::link(text="My account", href="/account/") }}
|
{{ button.link(text="My account", href="/account/") }}
|
||||||
{{ logout::button(text="Sign out", csrf_token=csrf_token) }}
|
{{ logout.button(text="Sign out", csrf_token=csrf_token) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ button::link(text="Sign in", href="/login") }}
|
{{ button.link(text="Sign in", href="/login") }}
|
||||||
{{ button::link_outline(text="Create an account", href="/register") }}
|
{{ button.link_outline(text="Create an account", href="/register") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
{% endmacro top %}
|
{% endmacro %}
|
||||||
|
@@ -16,23 +16,23 @@ limitations under the License.
|
|||||||
|
|
||||||
{% macro list(scopes) %}
|
{% macro list(scopes) %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for scope in scopes | split(pat=" ") %}
|
{% for scope in (scopes | split(" ")) %}
|
||||||
{% if scope == "openid" %}
|
{% if scope == "openid" %}
|
||||||
<li>{{ icon::user_profile() }}<p>See your profile info and contact details</p></li>
|
<li>{{ icon.user_profile() }}<p>See your profile info and contact details</p></li>
|
||||||
{% elif scope == "urn:mas:graphql:*" %}
|
{% elif scope == "urn:mas:graphql:*" %}
|
||||||
<li>{{ icon::info() }}<p>Edit your profile and contact details</p></li>
|
<li>{{ icon.info() }}<p>Edit your profile and contact details</p></li>
|
||||||
<li>{{ icon::computer() }}<p>Manage your devices and sessions</p></li>
|
<li>{{ icon.computer() }}<p>Manage your devices and sessions</p></li>
|
||||||
{% elif scope == "urn:matrix:org.matrix.msc2967.client:api:*" %}
|
{% elif scope == "urn:matrix:org.matrix.msc2967.client:api:*" %}
|
||||||
<li>{{ icon::chat() }}<p>View your existing messages and data</p></li>
|
<li>{{ icon.chat() }}<p>View your existing messages and data</p></li>
|
||||||
<li>{{ icon::check_circle() }}<p>Send new messages on your behalf</p></li>
|
<li>{{ icon.check_circle() }}<p>Send new messages on your behalf</p></li>
|
||||||
{% elif scope == "urn:synapse:admin:*" %}
|
{% elif scope == "urn:synapse:admin:*" %}
|
||||||
<li>{{ icon::error() }}<p>Administer the Synapse homeserver</p></li>
|
<li>{{ icon.error() }}<p>Administer the Synapse homeserver</p></li>
|
||||||
{% elif scope == "urn:mas:admin" %}
|
{% elif scope == "urn:mas:admin" %}
|
||||||
<li>{{ icon::error() }}<p>Administer any user on the MAS authentication server</p></li>
|
<li>{{ icon.error() }}<p>Administer any user on the MAS authentication server</p></li>
|
||||||
{% elif scope is matching("^urn:matrix:org.matrix.msc2967.client:device:") %}
|
{% elif scope is starting_with("urn:matrix:org.matrix.msc2967.client:device:") %}
|
||||||
{# We hide this scope #}
|
{# We hide this scope #}
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>{{ icon::info() }}<p>{{ scope }}</p></li>
|
<li>{{ icon.info() }}<p>{{ scope }}</p></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@@ -23,7 +23,7 @@ limitations under the License.
|
|||||||
</head>
|
</head>
|
||||||
<body onload="javascript:document.forms[0].submit()">
|
<body onload="javascript:document.forms[0].submit()">
|
||||||
<form method="post" action="{{ redirect_uri }}">
|
<form method="post" action="{{ redirect_uri }}">
|
||||||
{% for key, value in params %}
|
{% for key, value in params|items %}
|
||||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</form>
|
</form>
|
||||||
|
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ navbar::top() }}
|
{{ navbar.top() }}
|
||||||
<section class="flex items-center justify-center flex-1">
|
<section class="flex items-center justify-center flex-1">
|
||||||
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -27,13 +27,13 @@ limitations under the License.
|
|||||||
{% if form.errors is not empty %}
|
{% if form.errors is not empty %}
|
||||||
{% for error in form.errors %}
|
{% for error in form.errors %}
|
||||||
<div class="text-critical font-medium">
|
<div class="text-critical font-medium">
|
||||||
{{ errors::form_error_message(error=error) }}
|
{{ errors.form_error_message(error=error) }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
{{ field::input(label="Email", name="email", type="email", form_state=form, autocomplete="email", required=true) }}
|
{{ field.input(label="Email", name="email", type="email", form_state=form, autocomplete="email", required=true) }}
|
||||||
{{ button::button(text="Next") }}
|
{{ button.button(text="Next") }}
|
||||||
</section>
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ navbar::top() }}
|
{{ navbar.top() }}
|
||||||
<section class="flex items-center justify-center flex-1">
|
<section class="flex items-center justify-center flex-1">
|
||||||
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
@@ -28,13 +28,13 @@ limitations under the License.
|
|||||||
{% if form.errors is not empty %}
|
{% if form.errors is not empty %}
|
||||||
{% for error in form.errors %}
|
{% for error in form.errors %}
|
||||||
<div class="text-critical font-medium">
|
<div class="text-critical font-medium">
|
||||||
{{ errors::form_error_message(error=error) }}
|
{{ errors.form_error_message(error=error) }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
{{ field::input(label="Code", name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }}
|
{{ field.input(label="Code", name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }}
|
||||||
{{ button::button(text="Submit") }}
|
{{ button.button(text="Submit") }}
|
||||||
</section>
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@@ -17,15 +17,15 @@ limitations under the License.
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ navbar::top() }}
|
{{ navbar.top() }}
|
||||||
<section class="container mx-auto grid gap-4 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 py-2 px-8">
|
<section class="container mx-auto grid gap-4 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 py-2 px-8">
|
||||||
<form class="rounded border-2 border-grey-50 dark:border-grey-450 p-4 grid gap-4 xl:grid-cols-2 grid-cols-1 place-content-start" method="POST">
|
<form class="rounded border-2 border-grey-50 dark:border-grey-450 p-4 grid gap-4 xl:grid-cols-2 grid-cols-1 place-content-start" method="POST">
|
||||||
<h2 class="text-xl font-semibold xl:col-span-2">Change my password</h2>
|
<h2 class="text-xl font-semibold xl:col-span-2">Change my password</h2>
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
{{ field::input(label="Current password", name="current_password", type="password", autocomplete="current-password", class="xl:col-span-2") }}
|
{{ field.input(label="Current password", name="current_password", type="password", autocomplete="current-password", class="xl:col-span-2") }}
|
||||||
{{ field::input(label="New password", name="new_password", type="password", autocomplete="new-password") }}
|
{{ field.input(label="New password", name="new_password", type="password", autocomplete="new-password") }}
|
||||||
{{ field::input(label="Confirm password", name="new_password_confirm", type="password", autocomplete="new-password") }}
|
{{ field.input(label="Confirm password", name="new_password_confirm", type="password", autocomplete="new-password") }}
|
||||||
{{ button::button(text="Change password", type="submit", class="xl:col-span-2 place-self-end") }}
|
{{ button.button(text="Change password", type="submit", class="xl:col-span-2 place-self-end") }}
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% set client_name = client.client_name | default(value=client.client_id) %}
|
{% set client_name = client.client_name | default(client.client_id) %}
|
||||||
<section class="flex items-center justify-center flex-1">
|
<section class="flex items-center justify-center flex-1">
|
||||||
<div class="w-96 mx-2 my-8 flex flex-col gap-6">
|
<div class="w-96 mx-2 my-8 flex flex-col gap-6">
|
||||||
<div class="flex flex-col gap-2 text-center">
|
<div class="flex flex-col gap-2 text-center">
|
||||||
@@ -25,7 +25,7 @@ limitations under the License.
|
|||||||
<img class="consent-client-icon image" referrerpolicy="no-referrer" src="{{ client.logo_uri }}" />
|
<img class="consent-client-icon image" referrerpolicy="no-referrer" src="{{ client.logo_uri }}" />
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="consent-client-icon generic">
|
<div class="consent-client-icon generic">
|
||||||
{{ icon::web_browser() }}
|
{{ icon.web_browser() }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ limitations under the License.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="consent-scope-list">
|
<div class="consent-scope-list">
|
||||||
{{ scope::list(scopes=grant.scope) }}
|
{{ scope.list(scopes=grant.scope) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-2 text-center cpd-text-body-md-regular">
|
<div class="my-2 text-center cpd-text-body-md-regular">
|
||||||
@@ -56,10 +56,10 @@ limitations under the License.
|
|||||||
|
|
||||||
<form method="POST" class="flex flex-col">
|
<form method="POST" class="flex flex-col">
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
{{ button::button(text="Continue") }}
|
{{ button.button(text="Continue") }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ back_to_client::link(
|
{{ back_to_client.link(
|
||||||
text="Cancel",
|
text="Cancel",
|
||||||
kind="tertiary",
|
kind="tertiary",
|
||||||
uri=grant.redirect_uri,
|
uri=grant.redirect_uri,
|
||||||
@@ -69,7 +69,7 @@ limitations under the License.
|
|||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
Not {{ current_session.user.username }}?
|
Not {{ current_session.user.username }}?
|
||||||
{{ logout::button(text="Sign out", csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
{{ logout.button(text="Sign out", csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ navbar::top() }}
|
{{ navbar.top() }}
|
||||||
<section class="flex-1 flex flex-col items-center justify-center">
|
<section class="flex-1 flex flex-col items-center justify-center">
|
||||||
<div class="my-2 mx-8">
|
<div class="my-2 mx-8">
|
||||||
<h1 class="my-2 text-5xl font-semibold leading-tight">Matrix Authentication Service</h1>
|
<h1 class="my-2 text-5xl font-semibold leading-tight">Matrix Authentication Service</h1>
|
||||||
|
@@ -35,36 +35,36 @@ limitations under the License.
|
|||||||
{% if form.errors is not empty %}
|
{% if form.errors is not empty %}
|
||||||
{% for error in form.errors %}
|
{% for error in form.errors %}
|
||||||
<div class="text-critical font-medium">
|
<div class="text-critical font-medium">
|
||||||
{{ errors::form_error_message(error=error) }}
|
{{ errors.form_error_message(error=error) }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
{{ field::input(label="Username", name="username", form_state=form, autocomplete="username", autocorrect="off", autocapitalize="none") }}
|
{{ field.input(label="Username", name="username", form_state=form, autocomplete="username", autocorrect="off", autocapitalize="none") }}
|
||||||
{{ field::input(label="Password", name="password", type="password", form_state=form, autocomplete="password") }}
|
{{ field.input(label="Password", name="password", type="password", form_state=form, autocomplete="password") }}
|
||||||
{% if next and next.kind == "continue_authorization_grant" %}
|
{% if next and next.kind == "continue_authorization_grant" %}
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
{{ back_to_client::link(
|
{{ back_to_client.link(
|
||||||
text="Cancel",
|
text="Cancel",
|
||||||
kind="destructive",
|
kind="destructive",
|
||||||
uri=next.grant.redirect_uri,
|
uri=next.grant.redirect_uri,
|
||||||
mode=next.grant.response_mode,
|
mode=next.grant.response_mode,
|
||||||
params=dict(error="access_denied", state=next.grant.state)
|
params=dict(error="access_denied", state=next.grant.state)
|
||||||
) }}
|
) }}
|
||||||
{{ button::button(text="Next") }}
|
{{ button.button(text="Next") }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
{{ button::button(text="Next") }}
|
{{ button.button(text="Next") }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if not next or next.kind != "link_upstream" %}
|
{% if not next or next.kind != "link_upstream" %}
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
Don't have an account yet?
|
Don't have an account yet?
|
||||||
{% set params = next | safe_get(key="params") | to_params(prefix="?") %}
|
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
|
||||||
{{ button::link_text(text="Create an account", href="/register" ~ params) }}
|
{{ button.link_text(text="Create an account", href="/register" ~ params) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -79,8 +79,8 @@ limitations under the License.
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for provider in providers %}
|
{% for provider in providers %}
|
||||||
{% set params = next | safe_get(key="params") | to_params(prefix="?") %}
|
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
|
||||||
{{ button::link(text="Continue with " ~ provider.issuer, href="/upstream/authorize/" ~ provider.id ~ params) }}
|
{{ button.link(text="Continue with " ~ provider.issuer, href="/upstream/authorize/" ~ provider.id ~ params) }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@@ -28,7 +28,7 @@ limitations under the License.
|
|||||||
<img referrerpolicy="no-referrer" class="w-16 h-16" src="{{ client.logo_uri }}" />
|
<img referrerpolicy="no-referrer" class="w-16 h-16" src="{{ client.logo_uri }}" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-lg text-center font-medium flex-1"><a target="_blank" href="{{ client.client_uri }}" class="cpd-link" data-kind="primary">{{ client.client_name | default(value=client.client_id) }}</a></h1>
|
<h1 class="text-lg text-center font-medium flex-1"><a target="_blank" href="{{ client.client_uri }}" class="cpd-link" data-kind="primary">{{ client.client_name | default(client.client_id) }}</a></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex items-center">
|
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex items-center">
|
||||||
@@ -36,10 +36,10 @@ limitations under the License.
|
|||||||
Logged as <span class="font-semibold">{{ current_session.user.username }}</span>
|
Logged as <span class="font-semibold">{{ current_session.user.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ logout::button(text="Sign out", csrf_token=csrf_token, post_logout_action=action) }}
|
{{ logout.button(text="Sign out", csrf_token=csrf_token, post_logout_action=action) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ back_to_client::link(
|
{{ back_to_client.link(
|
||||||
text="Cancel",
|
text="Cancel",
|
||||||
kind="destructive",
|
kind="destructive",
|
||||||
uri=grant.redirect_uri,
|
uri=grant.redirect_uri,
|
||||||
|
@@ -26,28 +26,28 @@ limitations under the License.
|
|||||||
</div>
|
</div>
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
{# TODO: errors #}
|
{# TODO: errors #}
|
||||||
{{ field::input(label="Password", name="password", type="password", form_state=form, autocomplete="password") }}
|
{{ field.input(label="Password", name="password", type="password", form_state=form, autocomplete="password") }}
|
||||||
{% if next and next.kind == "continue_authorization_grant" %}
|
{% if next and next.kind == "continue_authorization_grant" %}
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
{{ back_to_client::link(
|
{{ back_to_client.link(
|
||||||
text="Cancel",
|
text="Cancel",
|
||||||
kind="destructive",
|
kind="destructive",
|
||||||
uri=next.grant.redirect_uri,
|
uri=next.grant.redirect_uri,
|
||||||
mode=next.grant.response_mode,
|
mode=next.grant.response_mode,
|
||||||
params=dict(error="access_denied", state=next.grant.state)
|
params=dict(error="access_denied", state=next.grant.state)
|
||||||
) }}
|
) }}
|
||||||
{{ button::button(text="Next") }}
|
{{ button.button(text="Next") }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
{{ button::button(text="Next") }}
|
{{ button.button(text="Next") }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
Not {{ current_session.user.username }}?
|
Not {{ current_session.user.username }}?
|
||||||
{% set post_logout_action = next | safe_get(key="params") %}
|
{% set post_logout_action = next["params"] | default({}) %}
|
||||||
{{ logout::button(text="Sign out", csrf_token=csrf_token, post_logout_action=post_logout_action, as_link=true) }}
|
{{ logout.button(text="Sign out", csrf_token=csrf_token, post_logout_action=post_logout_action, as_link=true) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -26,37 +26,37 @@ limitations under the License.
|
|||||||
{% if form.errors is not empty %}
|
{% if form.errors is not empty %}
|
||||||
{% for error in form.errors %}
|
{% for error in form.errors %}
|
||||||
<div class="text-critical font-medium">
|
<div class="text-critical font-medium">
|
||||||
{{ errors::form_error_message(error=error) }}
|
{{ errors.form_error_message(error=error) }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
{{ field::input(label="Username", name="username", form_state=form, autocomplete="username", autocorrect="off", autocapitalize="none") }}
|
{{ field.input(label="Username", name="username", form_state=form, autocomplete="username", autocorrect="off", autocapitalize="none") }}
|
||||||
{{ field::input(label="Email", name="email", type="email", form_state=form, autocomplete="email") }}
|
{{ field.input(label="Email", name="email", type="email", form_state=form, autocomplete="email") }}
|
||||||
{{ field::input(label="Password", name="password", type="password", form_state=form, autocomplete="new-password") }}
|
{{ field.input(label="Password", name="password", type="password", form_state=form, autocomplete="new-password") }}
|
||||||
{{ field::input(label="Confirm Password", name="password_confirm", type="password", form_state=form, autocomplete="new-password") }}
|
{{ field.input(label="Confirm Password", name="password_confirm", type="password", form_state=form, autocomplete="new-password") }}
|
||||||
|
|
||||||
{% if next and next.kind == "continue_authorization_grant" %}
|
{% if next and next.kind == "continue_authorization_grant" %}
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
{{ back_to_client::link(
|
{{ back_to_client.link(
|
||||||
text="Cancel",
|
text="Cancel",
|
||||||
kind="destructive",
|
kind="destructive",
|
||||||
uri=next.grant.redirect_uri,
|
uri=next.grant.redirect_uri,
|
||||||
mode=next.grant.response_mode,
|
mode=next.grant.response_mode,
|
||||||
params=dict(error="access_denied", state=next.grant.state)
|
params=dict(error="access_denied", state=next.grant.state)
|
||||||
) }}
|
) }}
|
||||||
{{ button::button(text="Next") }}
|
{{ button.button(text="Next") }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<div class="grid grid-cols-1 gap-4">
|
||||||
{{ button::button(text="Next") }}
|
{{ button.button(text="Next") }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="text-center mt-4">
|
<div class="text-center mt-4">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
{% set params = next | safe_get(key="params") | to_params(prefix="?") %}
|
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
|
||||||
{{ button::link_text(text="Sign in instead", href="/login" ~ params) }}
|
{{ button.link_text(text="Sign in instead", href="/login" ~ params) }}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -21,19 +21,15 @@ limitations under the License.
|
|||||||
<section class="flex items-center justify-center flex-1">
|
<section class="flex items-center justify-center flex-1">
|
||||||
<div class="w-96 mx-2 my-8 flex flex-col gap-6">
|
<div class="w-96 mx-2 my-8 flex flex-col gap-6">
|
||||||
<div class="flex flex-col gap-2 text-center">
|
<div class="flex flex-col gap-2 text-center">
|
||||||
{% if client.logo_uri %}
|
<div class="consent-client-icon generic">
|
||||||
<img class="consent-client-icon image" referrerpolicy="no-referrer" src="{{ client.logo_uri }}" />
|
{{ icon.web_browser() }}
|
||||||
{% else %}
|
</div>
|
||||||
<div class="consent-client-icon generic">
|
|
||||||
{{ icon::web_browser() }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p class="cpd-text-secondary cpd-text-body-lg-regular"><span class="whitespace-nowrap">{{ client_name }}</span> wants to access your account. This will allow <span class="whitespace-nowrap">{{ client_name }}</span> to:</p>
|
<p class="cpd-text-secondary cpd-text-body-lg-regular"><span class="whitespace-nowrap">{{ client_name }}</span> wants to access your account. This will allow <span class="whitespace-nowrap">{{ client_name }}</span> to:</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="consent-scope-list">
|
<div class="consent-scope-list">
|
||||||
{{ scope::list(scopes="openid urn:matrix:org.matrix.msc2967.client:api:*") }}
|
{{ scope.list(scopes="openid urn:matrix:org.matrix.msc2967.client:api:*") }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-2 text-center cpd-text-body-md-regular">
|
<div class="my-2 text-center cpd-text-body-md-regular">
|
||||||
@@ -43,12 +39,12 @@ limitations under the License.
|
|||||||
|
|
||||||
<form method="POST" class="flex flex-col">
|
<form method="POST" class="flex flex-col">
|
||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
{{ button::button(text="Continue") }}
|
{{ button.button(text="Continue") }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
Not {{ current_session.user.username }}?
|
Not {{ current_session.user.username }}?
|
||||||
{{ logout::button(text="Sign out", csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
{{ logout.button(text="Sign out", csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@@ -36,7 +36,7 @@ limitations under the License.
|
|||||||
<div class="font-mono">{{ suggested_localpart }}</div>
|
<div class="font-mono">{{ suggested_localpart }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ field::input(label="Username", name="username", autocomplete="username", autocorrect="off", autocapitalize="none") }}
|
{{ field.input(label="Username", name="username", autocomplete="username", autocorrect="off", autocapitalize="none") }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if suggested_email %}
|
{% if suggested_email %}
|
||||||
@@ -67,14 +67,14 @@ limitations under the License.
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ button::button(text="Create a new account") }}
|
{{ button.button(text="Create a new account") }}
|
||||||
</form>
|
</form>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<hr class="flex-1" />
|
<hr class="flex-1" />
|
||||||
<div class="mx-2">Or</div>
|
<div class="mx-2">Or</div>
|
||||||
<hr class="flex-1" />
|
<hr class="flex-1" />
|
||||||
</div>
|
</div>
|
||||||
{{ button::link_outline(text="Link to an existing account", href=login_link) }}
|
{{ button.link_outline(text="Link to an existing account", href=login_link) }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@@ -17,14 +17,14 @@ limitations under the License.
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ navbar::top() }}
|
{{ navbar.top() }}
|
||||||
<section class="flex items-center justify-center flex-1">
|
<section class="flex items-center justify-center flex-1">
|
||||||
<div class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
<div class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||||
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col font-medium text-lg text-center">
|
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col font-medium text-lg text-center">
|
||||||
This upstream account is already linked to another account.
|
This upstream account is already linked to another account.
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div>{{ logout::button(text="Logout", csrf_token=csrf_token) }}</div>
|
<div>{{ logout.button(text="Logout", csrf_token=csrf_token) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{{ navbar::top() }}
|
{{ navbar.top() }}
|
||||||
<section class="flex items-center justify-center flex-1">
|
<section class="flex items-center justify-center flex-1">
|
||||||
<div class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
<div class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||||
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col font-medium text-lg text-center">
|
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col font-medium text-lg text-center">
|
||||||
@@ -28,10 +28,10 @@ limitations under the License.
|
|||||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||||
<input type="hidden" name="action" value="link" />
|
<input type="hidden" name="action" value="link" />
|
||||||
|
|
||||||
{{ button::button(text="Link", class="flex-1") }}
|
{{ button.button(text="Link", class="flex-1") }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div>Or {{ logout::button(text="Logout", csrf_token=csrf_token, post_logout_action=post_logout_action, as_link=true) }}</div>
|
<div>Or {{ logout.button(text="Logout", csrf_token=csrf_token, post_logout_action=post_logout_action, as_link=true) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
Reference in New Issue
Block a user