diff --git a/Cargo.lock b/Cargo.lock index c7c2bac2..c1c82407 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "argon2" version = "0.5.2" @@ -3334,23 +3340,25 @@ name = "mas-templates" version = "0.2.0" dependencies = [ "anyhow", + "arc-swap", "camino", "chrono", "http", "mas-data-model", "mas-router", "mas-spa", + "minijinja", "oauth2-types", "rand 0.8.5", "serde", "serde_json", "serde_urlencoded", - "tera", "thiserror", "tokio", "tracing", "ulid", "url", + "walkdir", ] [[package]] @@ -3413,6 +3421,12 @@ dependencies = [ "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]] name = "memoffset" version = "0.9.0" @@ -3444,7 +3458,10 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80084fa3099f58b7afab51e5f92e24c2c2c68dcad26e96ad104bd6011570461d" dependencies = [ + "memo-map", + "self_cell", "serde", + "serde_json", ] [[package]] @@ -4906,6 +4923,12 @@ dependencies = [ "libc", ] +[[package]] +name = "self_cell" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" + [[package]] name = "semver" version = "1.0.18" diff --git a/Cargo.toml b/Cargo.toml index 91ab57a7..af30208d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,16 +17,16 @@ repository = "https://github.com/matrix-org/matrix-authentication-service/" [workspace.dependencies.anyhow] version = "1.0.75" +# UTF-8 paths +[workspace.dependencies.camino] +version = "1.1.6" + # Time utilities [workspace.dependencies.chrono] version = "0.4.31" default-features = false features = ["serde", "clock"] -# UTF-8 paths -[workspace.dependencies.camino] -version = "1.1.6" - # CLI argument parsing [workspace.dependencies.clap] version = "4.4.4" @@ -36,6 +36,10 @@ features = ["derive"] [workspace.dependencies.http] version = "0.2.9" +# Templates +[workspace.dependencies.minijinja] +version = "1.0.8" + # Random values [workspace.dependencies.rand] 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] version = "1.0.107" +# Templates +[workspace.dependencies.tera] +version = "1.19.1" +default-features = false + # Custom error types [workspace.dependencies.thiserror] version = "1.0.48" @@ -59,11 +68,6 @@ version = "0.1.37" [workspace.dependencies.tracing-subscriber] version = "0.3.17" -# Templates -[workspace.dependencies.tera] -version = "1.19.1" -default-features = false - # URL manipulation [workspace.dependencies.url] version = "2.4.1" diff --git a/crates/cli/src/commands/templates.rs b/crates/cli/src/commands/templates.rs index f97f8ca0..e2133309 100644 --- a/crates/cli/src/commands/templates.rs +++ b/crates/cli/src/commands/templates.rs @@ -46,7 +46,7 @@ impl Options { let url_builder = mas_router::UrlBuilder::new("https://example.com/".parse()?, None, None); 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(()) } diff --git a/crates/email/src/mailer.rs b/crates/email/src/mailer.rs index 979a9f7b..90bc2011 100644 --- a/crates/email/src/mailer.rs +++ b/crates/email/src/mailer.rs @@ -63,27 +63,18 @@ impl Mailer { .reply_to(self.reply_to.clone()) } - async fn prepare_verification_email( + fn prepare_verification_email( &self, to: Mailbox, context: &EmailVerificationContext, ) -> Result { - let plain = self - .templates - .render_email_verification_txt(context) - .await?; + let plain = self.templates.render_email_verification_txt(context)?; - let html = self - .templates - .render_email_verification_html(context) - .await?; + let html = self.templates.render_email_verification_html(context)?; let multipart = MultiPart::alternative_plain_html(plain, html); - let subject = self - .templates - .render_email_verification_subject(context) - .await?; + let subject = self.templates.render_email_verification_subject(context)?; let message = self .base_message() @@ -115,7 +106,7 @@ impl Mailer { to: Mailbox, context: &EmailVerificationContext, ) -> Result<(), Error> { - let message = self.prepare_verification_email(to, context).await?; + let message = self.prepare_verification_email(to, context)?; self.transport.send(message).await?; Ok(()) } diff --git a/crates/handlers/src/compat/login_sso_complete.rs b/crates/handlers/src/compat/login_sso_complete.rs index 8ed24a00..94d90f7b 100644 --- a/crates/handlers/src/compat/login_sso_complete.rs +++ b/crates/handlers/src/compat/login_sso_complete.rs @@ -104,7 +104,7 @@ pub async fn get( .with_code("compat_sso_login_expired") .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()); } @@ -112,7 +112,7 @@ pub async fn get( .with_session(session) .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()) } @@ -171,7 +171,7 @@ pub async fn post( .with_code("compat_sso_login_expired") .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()); } diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index df1e7cdd..97ea2e41 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -409,7 +409,7 @@ where // Error responses should have an ErrorContext attached to them let ext = response.extensions().get::(); 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(); parts.headers.remove(CONTENT_TYPE); parts.headers.remove(CONTENT_LENGTH); @@ -437,7 +437,7 @@ pub async fn fallback( let ctx = NotFoundContext::new(&method, version, &uri); // 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))) } diff --git a/crates/handlers/src/oauth2/authorization/callback.rs b/crates/handlers/src/oauth2/authorization/callback.rs index 515e1ff0..e7db5065 100644 --- a/crates/handlers/src/oauth2/authorization/callback.rs +++ b/crates/handlers/src/oauth2/authorization/callback.rs @@ -164,7 +164,7 @@ impl CallbackDestination { params, }; 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()) } } diff --git a/crates/handlers/src/oauth2/authorization/complete.rs b/crates/handlers/src/oauth2/authorization/complete.rs index ea9ea0eb..6419cb41 100644 --- a/crates/handlers/src/oauth2/authorization/complete.rs +++ b/crates/handlers/src/oauth2/authorization/complete.rs @@ -162,7 +162,7 @@ pub(crate) async fn get( .with_session(session) .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()) } diff --git a/crates/handlers/src/oauth2/authorization/mod.rs b/crates/handlers/src/oauth2/authorization/mod.rs index 07516586..f4b9ed8f 100644 --- a/crates/handlers/src/oauth2/authorization/mod.rs +++ b/crates/handlers/src/oauth2/authorization/mod.rs @@ -420,7 +420,7 @@ pub(crate) async fn get( .with_session(user_session) .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() } Err(GrantCompletionError::RequiresReauth) => { diff --git a/crates/handlers/src/oauth2/consent.rs b/crates/handlers/src/oauth2/consent.rs index 2e96becd..06fe5075 100644 --- a/crates/handlers/src/oauth2/consent.rs +++ b/crates/handlers/src/oauth2/consent.rs @@ -125,7 +125,7 @@ pub(crate) async fn get( .with_session(session) .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()) } else { @@ -133,7 +133,7 @@ pub(crate) async fn get( .with_session(session) .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()) } diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index c8fa1052..7c61e494 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -266,7 +266,7 @@ pub(crate) async fn get( .with_session(user_session) .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) => { @@ -275,7 +275,7 @@ pub(crate) async fn get( .with_session(user_session) .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)) => { @@ -360,7 +360,7 @@ pub(crate) async fn get( 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() } }; diff --git a/crates/handlers/src/views/account/emails/add.rs b/crates/handlers/src/views/account/emails/add.rs index f691af42..6778ee19 100644 --- a/crates/handlers/src/views/account/emails/add.rs +++ b/crates/handlers/src/views/account/emails/add.rs @@ -65,7 +65,7 @@ pub(crate) async fn get( .with_session(session) .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()) } diff --git a/crates/handlers/src/views/account/emails/verify.rs b/crates/handlers/src/views/account/emails/verify.rs index a138ef06..f432bae2 100644 --- a/crates/handlers/src/views/account/emails/verify.rs +++ b/crates/handlers/src/views/account/emails/verify.rs @@ -86,7 +86,7 @@ pub(crate) async fn get( .with_session(session) .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()) } diff --git a/crates/handlers/src/views/account/password.rs b/crates/handlers/src/views/account/password.rs index c5bb1193..b33c7490 100644 --- a/crates/handlers/src/views/account/password.rs +++ b/crates/handlers/src/views/account/password.rs @@ -88,7 +88,7 @@ async fn render( .with_session(session) .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()) } diff --git a/crates/handlers/src/views/app.rs b/crates/handlers/src/views/app.rs index f171c204..6f25d936 100644 --- a/crates/handlers/src/views/app.rs +++ b/crates/handlers/src/views/app.rs @@ -50,7 +50,7 @@ pub async fn get( .await; let ctx = AppContext::default(); - let content = templates.render_app(&ctx).await?; + let content = templates.render_app(&ctx)?; Ok((cookie_jar, Html(content)).into_response()) } diff --git a/crates/handlers/src/views/index.rs b/crates/handlers/src/views/index.rs index 0e1fa3f1..349d1ed0 100644 --- a/crates/handlers/src/views/index.rs +++ b/crates/handlers/src/views/index.rs @@ -47,7 +47,7 @@ pub async fn get( .maybe_with_session(session) .with_csrf(csrf_token.form_value()); - let content = templates.render_index(&ctx).await?; + let content = templates.render_index(&ctx)?; tracing::info!("rendered index page"); diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index 366da930..4e5fda7c 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -289,7 +289,7 @@ async fn render( }; let ctx = ctx.with_csrf(csrf_token.form_value()); - let content = templates.render_login(&ctx).await?; + let content = templates.render_login(&ctx)?; Ok(content) } diff --git a/crates/handlers/src/views/reauth.rs b/crates/handlers/src/views/reauth.rs index 54e41c9a..4944d7f4 100644 --- a/crates/handlers/src/views/reauth.rs +++ b/crates/handlers/src/views/reauth.rs @@ -81,7 +81,7 @@ pub(crate) async fn get( }; 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()) } diff --git a/crates/handlers/src/views/register.rs b/crates/handlers/src/views/register.rs index 1cd3f2b3..096585d8 100644 --- a/crates/handlers/src/views/register.rs +++ b/crates/handlers/src/views/register.rs @@ -252,7 +252,7 @@ async fn render( }; let ctx = ctx.with_csrf(csrf_token.form_value()); - let content = templates.render_register(&ctx).await?; + let content = templates.render_register(&ctx)?; Ok(content) } diff --git a/crates/i18n-scan/Cargo.toml b/crates/i18n-scan/Cargo.toml index 45e0c6d4..8f22b773 100644 --- a/crates/i18n-scan/Cargo.toml +++ b/crates/i18n-scan/Cargo.toml @@ -10,7 +10,7 @@ repository.workspace = true [dependencies] camino.workspace = true clap.workspace = true -minijinja = { version = "1.0.8", features = ["unstable_machinery"] } +minijinja = { workspace = true, features = ["unstable_machinery"] } serde_json.workspace = true tera.workspace = true tracing-subscriber.workspace = true diff --git a/crates/templates/Cargo.toml b/crates/templates/Cargo.toml index 6162be77..3964f2f2 100644 --- a/crates/templates/Cargo.toml +++ b/crates/templates/Cargo.toml @@ -8,13 +8,15 @@ homepage.workspace = true repository.workspace = true [dependencies] +arc-swap = "1.6.0" tracing.workspace = true tokio = { version = "1.32.0", features = ["macros", "rt", "fs"] } +walkdir = "2.4.0" anyhow.workspace = true thiserror.workspace = true -tera.workspace = true +minijinja = { workspace = true, features = ["loader", "json"] } serde.workspace = true serde_json.workspace = true serde_urlencoded = "0.7.1" diff --git a/crates/templates/src/functions.rs b/crates/templates/src/functions.rs index dcecca48..895f12eb 100644 --- a/crates/templates/src/functions.rs +++ b/crates/templates/src/functions.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // 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 use std::{ @@ -22,78 +25,80 @@ use std::{ use camino::Utf8Path; use mas_router::UrlBuilder; 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; -pub fn register(tera: &mut Tera, url_builder: UrlBuilder, vite_manifest: ViteManifest) { - tera.register_tester("empty", self::tester_empty); - tera.register_filter("to_params", filter_to_params); - tera.register_filter("safe_get", filter_safe_get); - tera.register_filter("simplify_url", filter_simplify_url); - tera.register_function("add_params_to_url", function_add_params_to_url); - tera.register_function("merge", function_merge); - tera.register_function("dict", function_dict); - tera.register_function( +pub fn register( + env: &mut minijinja::Environment, + url_builder: UrlBuilder, + vite_manifest: ViteManifest, +) { + env.add_test("empty", self::tester_empty); + env.add_test("starting_with", tester_starting_with); + env.add_filter("to_params", filter_to_params); + 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", - IncludeAsset { + Value::from_object(IncludeAsset { url_builder, vite_manifest, - }, + }), ); } -fn tester_empty(value: Option<&Value>, params: &[Value]) -> Result { - number_args_allowed("empty", 0, params.len())?; - - match value.and_then(Value::as_array).map(|v| &v[..]) { - Some(&[]) | None => Ok(true), - Some(_) => Ok(false), - } +fn tester_empty(seq: &dyn SeqObject) -> bool { + seq.item_count() == 0 } -fn filter_to_params(params: &Value, kv: &HashMap) -> Result { - let prefix = kv.get("prefix").and_then(Value::as_str).unwrap_or(""); - let params = serde_urlencoded::to_string(params) - .map_err(|e| tera::Error::chain(e, "Could not serialize parameters"))?; +fn tester_starting_with(value: &str, prefix: &str) -> bool { + value.starts_with(prefix) +} + +fn filter_split(value: &str, separator: &str) -> Vec { + 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 { + 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() { - Ok(Value::String(String::new())) + Ok(String::new()) } else { - Ok(Value::String(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) -> Result { - 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()), + Ok(format!("{prefix}{params}")) } } /// Filter which simplifies a URL to its domain name for HTTP(S) URLs -fn filter_simplify_url(value: &Value, args: &HashMap) -> Result { - 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")); - } - +fn filter_simplify_url(url: &str) -> String { // Do nothing if the URL is not valid 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 @@ -102,15 +107,15 @@ fn filter_simplify_url(value: &Value, args: &HashMap) -> Result) -> Result { +fn function_add_params_to_url( + uri: ViaDeserialize, + mode: &str, + params: ViaDeserialize>, +) -> Result { 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 { "fragment" => Fragment, "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 let existing = match mode { Fragment => uri.fragment(), @@ -149,15 +150,26 @@ fn function_add_params_to_url(params: &HashMap) -> Result = existing .map(serde_urlencoded::from_str) .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(); // Merge the exising and the additional parameters together let params: HashMap<&String, &Value> = params.iter().chain(existing.iter()).collect(); // Transform them back to urlencoded - let params = serde_urlencoded::to_string(params) - .map_err(|e| tera::Error::chain(e, "Could not serialize back parameters"))?; + let params = serde_urlencoded::to_string(params).map_err(|e| { + Error::new( + ErrorKind::InvalidOperation, + "Could not serialize back parameters", + ) + .with_source(e) + })?; let uri = { let mut uri = uri; @@ -168,9 +180,10 @@ fn function_add_params_to_url(params: &HashMap) -> Result) -> Result { let mut ret = serde_json::Map::new(); for (k, v) in params { @@ -182,50 +195,41 @@ fn function_merge(params: &HashMap) -> Result Ok(Value::Object(ret)) } + */ -#[allow(clippy::unnecessary_wraps)] -fn function_dict(params: &HashMap) -> Result { - let ret = params.clone().into_iter().collect(); - Ok(Value::Object(ret)) -} - +#[derive(Debug)] struct IncludeAsset { url_builder: UrlBuilder, vite_manifest: ViteManifest, } -impl tera::Function for IncludeAsset { - fn call(&self, args: &HashMap) -> tera::Result { - let path = args.get("path").ok_or(tera::Error::msg( - "Function `include_asset` was missing parameter `path`", - ))?; +impl std::fmt::Display for IncludeAsset { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("include_asset") + } +} - let preload = args - .get("preload") - .and_then(Value::as_bool) - .unwrap_or(false); +impl Object for IncludeAsset { + fn call(&self, _state: &State, args: &[Value]) -> Result { + let (path, kwargs): (&str, Kwargs) = from_args(args)?; - let path: &Utf8Path = path - .as_str() - .ok_or_else(|| { - tera::Error::msg( - "Function `include_asset` received an incorrect type for arg `path`", - ) - })? - .into(); + let preload = kwargs.get("preload").unwrap_or(false); + kwargs.assert_all_used()?; - let assets = self.vite_manifest.assets_for(path).map_err(|e| { - tera::Error::chain( + let path: &Utf8Path = path.into(); + + let assets = self.vite_manifest.assets_for(path).map_err(|_e| { + Error::new( + ErrorKind::InvalidOperation, "Invalid assets manifest while calling function `include_asset`", - e.to_string(), ) })?; let preloads = if preload { - self.vite_manifest.preload_for(path).map_err(|e| { - tera::Error::chain( + self.vite_manifest.preload_for(path).map_err(|_e| { + Error::new( + ErrorKind::InvalidOperation, "Invalid assets manifest while calling function `include_asset`", - e.to_string(), ) })? } else { @@ -242,10 +246,6 @@ impl tera::Function for IncludeAsset { ) .collect(); - Ok(Value::String(tags.join("\n"))) - } - - fn is_safe(&self) -> bool { - true + Ok(Value::from_safe_string(tags.join("\n"))) } } diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index f4ba609f..d3741e17 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -24,19 +24,19 @@ //! Templates rendering -use std::{collections::HashSet, string::ToString, sync::Arc}; +use std::{collections::HashSet, sync::Arc}; use anyhow::Context as _; +use arc_swap::ArcSwap; use camino::{Utf8Path, Utf8PathBuf}; use mas_router::UrlBuilder; use mas_spa::ViteManifest; use rand::Rng; use serde::Serialize; -pub use tera::escape_html; -use tera::{Context, Error as TeraError, Tera}; use thiserror::Error; -use tokio::{sync::RwLock, task::JoinError}; +use tokio::task::JoinError; use tracing::{debug, info, warn}; +use walkdir::DirEntry; mod context; mod forms; @@ -57,10 +57,11 @@ pub use self::{ 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)] pub struct Templates { - tera: Arc>, + environment: Arc>>, url_builder: UrlBuilder, vite_manifest_path: Utf8PathBuf, path: Utf8PathBuf, @@ -81,9 +82,25 @@ pub enum TemplateLoadingError { #[error("invalid assets manifest")] 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 #[error("could not load and compile some templates")] - Compile(#[from] TeraError), + Compile(#[from] minijinja::Error), /// Could not join blocking task #[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 { /// Load the templates from the given config #[tracing::instrument( @@ -112,9 +136,9 @@ impl Templates { url_builder: UrlBuilder, vite_manifest_path: Utf8PathBuf, ) -> Result { - 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 { - tera: Arc::new(RwLock::new(tera)), + environment: Arc::new(ArcSwap::from_pointee(environment)), path, url_builder, vite_manifest_path, @@ -125,7 +149,7 @@ impl Templates { path: &Utf8Path, url_builder: UrlBuilder, vite_manifest_path: &Utf8Path, - ) -> Result { + ) -> Result, TemplateLoadingError> { let path = path.to_owned(); let span = tracing::Span::current(); @@ -138,30 +162,48 @@ impl Templates { let vite_manifest: ViteManifest = serde_json::from_slice(&vite_manifest).map_err(TemplateLoadingError::ViteManifest)?; - // This uses blocking I/Os, do that in a blocking task - let mut tera = tokio::task::spawn_blocking(move || { + let (loaded, mut env) = tokio::task::spawn_blocking(move || { span.in_scope(move || { - let path = path.canonicalize_utf8()?; - let path = format!("{path}/**/*.{{html,txt,subject}}"); + let mut loaded: HashSet<_> = HashSet::new(); + 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"); - Tera::new(&path) + if ext == "html" || ext == "txt" || ext == "subject" { + 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??; - 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().collect(); + let needed: HashSet<_> = TEMPLATES.into_iter().map(ToOwned::to_owned).collect(); debug!(?loaded, ?needed, "Templates loaded"); - let missing: HashSet<_> = needed.difference(&loaded).collect(); + let missing: HashSet<_> = needed.difference(&loaded).cloned().collect(); if missing.is_empty() { - Ok(tera) + Ok(env) } 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 }) } } @@ -174,8 +216,7 @@ impl Templates { err, )] pub async fn reload(&self) -> Result<(), TemplateLoadingError> { - // Prepare the new Tera instance - let new_tera = Self::load_( + let new_minijinja = Self::load_( &self.path, self.url_builder.clone(), &self.vite_manifest_path, @@ -183,7 +224,7 @@ impl Templates { .await?; // Swap it - *self.tera.write().await = new_tera; + self.environment.store(Arc::new(new_minijinja)); Ok(()) } @@ -192,15 +233,15 @@ impl Templates { /// Failed to render a template #[derive(Error, Debug)] pub enum TemplateError { - /// Failed to prepare the context used by this template - #[error("could not prepare context for template {template:?}")] - Context { + /// Missing template + #[error("missing template {template:?}")] + Missing { /// The name of the template being rendered template: &'static str, /// The underlying error #[source] - source: TeraError, + source: minijinja::Error, }, /// Failed to render the template @@ -211,7 +252,7 @@ pub enum TemplateError { /// The underlying error #[source] - source: TeraError, + source: minijinja::Error, }, } @@ -280,31 +321,31 @@ register_templates! { impl Templates { /// Render all templates with the generated samples to check if they render /// properly - pub async fn check_render( + pub fn check_render( &self, now: chrono::DateTime, rng: &mut impl Rng, ) -> anyhow::Result<()> { - check::render_not_found(self, now, rng).await?; - check::render_app(self, now, rng).await?; - check::render_login(self, now, rng).await?; - check::render_register(self, now, rng).await?; - check::render_consent(self, now, rng).await?; - check::render_policy_violation(self, now, rng).await?; - check::render_sso_login(self, now, rng).await?; - check::render_index(self, now, rng).await?; - check::render_account_password(self, now, rng).await?; - check::render_account_add_email(self, now, rng).await?; - check::render_account_verify_email(self, now, rng).await?; - check::render_reauth(self, now, rng).await?; - check::render_form_post::(self, now, rng).await?; - check::render_error(self, now, rng).await?; - check::render_email_verification_txt(self, now, rng).await?; - check::render_email_verification_html(self, now, rng).await?; - check::render_email_verification_subject(self, now, rng).await?; - check::render_upstream_oauth2_link_mismatch(self, now, rng).await?; - check::render_upstream_oauth2_suggest_link(self, now, rng).await?; - check::render_upstream_oauth2_do_register(self, now, rng).await?; + check::render_not_found(self, now, rng)?; + check::render_app(self, now, rng)?; + check::render_login(self, now, rng)?; + check::render_register(self, now, rng)?; + check::render_consent(self, now, rng)?; + check::render_policy_violation(self, now, rng)?; + check::render_sso_login(self, now, rng)?; + check::render_index(self, now, rng)?; + check::render_account_password(self, now, rng)?; + check::render_account_add_email(self, now, rng)?; + check::render_account_verify_email(self, now, rng)?; + check::render_reauth(self, now, rng)?; + check::render_form_post::(self, now, rng)?; + check::render_error(self, now, rng)?; + check::render_email_verification_txt(self, now, rng)?; + check::render_email_verification_html(self, now, rng)?; + check::render_email_verification_subject(self, now, rng)?; + check::render_upstream_oauth2_link_mismatch(self, now, rng)?; + check::render_upstream_oauth2_suggest_link(self, now, rng)?; + check::render_upstream_oauth2_do_register(self, now, rng)?; Ok(()) } } @@ -327,6 +368,6 @@ mod tests { let templates = Templates::load(path, url_builder, vite_manifest_path) .await .unwrap(); - templates.check_render(now, &mut rng).await.unwrap(); + templates.check_render(now, &mut rng).unwrap(); } } diff --git a/crates/templates/src/macros.rs b/crates/templates/src/macros.rs index dd9a2d4e..32da537e 100644 --- a/crates/templates/src/macros.rs +++ b/crates/templates/src/macros.rs @@ -54,14 +54,16 @@ macro_rules! register_templates { impl Templates { $( $(#[$attr])? - pub async fn $name + pub fn $name $(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? (&self, context: &$param) -> Result { - let ctx = Context::from_serialize(context) - .map_err(|source| TemplateError::Context { template: $template, source })?; + let ctx = ::minijinja::value::Value::from_serializable(context); - 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 }) } )* @@ -73,7 +75,7 @@ macro_rules! register_templates { $( #[doc = concat!("Render the `", $template, "` template with sample contexts")] - pub async fn $name + pub fn $name $(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)? (templates: &Templates, now: chrono::DateTime, rng: &mut impl rand::Rng) -> anyhow::Result<()> { @@ -84,7 +86,6 @@ macro_rules! register_templates { let context = serde_json::to_value(&sample)?; ::tracing::info!(name, %context, "Rendering template"); templates. $name (&sample) - .await .with_context(|| format!("Failed to render template {:?} with context {}", name, context))?; } diff --git a/templates/app.html b/templates/app.html index 7a7c9ba8..c6f899c0 100644 --- a/templates/app.html +++ b/templates/app.html @@ -23,7 +23,7 @@ limitations under the License. matrix-authentication-service - {{ include_asset(path='src/main.tsx', preload=true) | indent(prefix=" ") | safe }} + {{ include_asset('src/main.tsx', preload=true) | indent(4) | safe }} diff --git a/templates/base.html b/templates/base.html index 95cf6b6c..2164d2b0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -29,7 +29,7 @@ limitations under the License. {% block title %}matrix-authentication-service{% endblock title %} - {{ include_asset(path='src/templates.css', preload=true) | indent(prefix=" ") | safe }} + {{ include_asset('src/templates.css', preload=true) | indent(4) | safe }} {% block content %}{% endblock content %} diff --git a/templates/components/back_to_client.html b/templates/components/back_to_client.html index c273e743..3f4780b2 100644 --- a/templates/components/back_to_client.html +++ b/templates/components/back_to_client.html @@ -25,13 +25,13 @@ limitations under the License. {% if mode == "form_post" %}
- {% for key, value in params %} + {% for key, value in params|items %} {% endfor %}
{% elif mode == "fragment" or mode == "query" %} - {{ text }} + {{ text }} {% else %} {{ throw(message="Invalid mode") }} {% endif %} diff --git a/templates/components/field.html b/templates/components/field.html index f5eac7fd..1a505b27 100644 --- a/templates/components/field.html +++ b/templates/components/field.html @@ -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) %} {% if not form_state %} - {% set form_state = dict(errors=[], fields=dict()) %} + {% set form_state = {"errors": [], "fields": {}} %} {% endif %} - {% set state = form_state.fields[name] | default(value=dict(errors=[], value="")) %} + {% set state = form_state.fields[name] | default({"errors": [], "value": ""}) %}
{{ label }}
@@ -53,4 +53,4 @@ limitations under the License. {% endfor %} {% endif %}
-{% endmacro input %} +{% endmacro %} diff --git a/templates/components/logout.html b/templates/components/logout.html index f9c19aeb..7d84cf7e 100644 --- a/templates/components/logout.html +++ b/templates/components/logout.html @@ -14,14 +14,12 @@ See the License for the specific language governing permissions and 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={}) %}
- {% if post_logout_action %} - {% for key, value in post_logout_action %} - - {% endfor %} - {% endif %} + {% for key, value in post_logout_action|items %} + + {% endfor %}
{% endmacro %} diff --git a/templates/components/navbar.html b/templates/components/navbar.html index 91ae7c80..e641dbfd 100644 --- a/templates/components/navbar.html +++ b/templates/components/navbar.html @@ -24,12 +24,12 @@ limitations under the License. Signed in as {{ current_session.user.username }}. - {{ button::link(text="My account", href="/account/") }} - {{ logout::button(text="Sign out", csrf_token=csrf_token) }} + {{ button.link(text="My account", href="/account/") }} + {{ logout.button(text="Sign out", csrf_token=csrf_token) }} {% else %} - {{ button::link(text="Sign in", href="/login") }} - {{ button::link_outline(text="Create an account", href="/register") }} + {{ button.link(text="Sign in", href="/login") }} + {{ button.link_outline(text="Create an account", href="/register") }} {% endif %} -{% endmacro top %} +{% endmacro %} diff --git a/templates/components/scope.html b/templates/components/scope.html index d3216bda..fb114fba 100644 --- a/templates/components/scope.html +++ b/templates/components/scope.html @@ -16,23 +16,23 @@ limitations under the License. {% macro list(scopes) %}
    - {% for scope in scopes | split(pat=" ") %} + {% for scope in (scopes | split(" ")) %} {% if scope == "openid" %} -
  • {{ icon::user_profile() }}

    See your profile info and contact details

  • +
  • {{ icon.user_profile() }}

    See your profile info and contact details

  • {% elif scope == "urn:mas:graphql:*" %} -
  • {{ icon::info() }}

    Edit your profile and contact details

  • -
  • {{ icon::computer() }}

    Manage your devices and sessions

  • +
  • {{ icon.info() }}

    Edit your profile and contact details

  • +
  • {{ icon.computer() }}

    Manage your devices and sessions

  • {% elif scope == "urn:matrix:org.matrix.msc2967.client:api:*" %} -
  • {{ icon::chat() }}

    View your existing messages and data

  • -
  • {{ icon::check_circle() }}

    Send new messages on your behalf

  • +
  • {{ icon.chat() }}

    View your existing messages and data

  • +
  • {{ icon.check_circle() }}

    Send new messages on your behalf

  • {% elif scope == "urn:synapse:admin:*" %} -
  • {{ icon::error() }}

    Administer the Synapse homeserver

  • +
  • {{ icon.error() }}

    Administer the Synapse homeserver

  • {% elif scope == "urn:mas:admin" %} -
  • {{ icon::error() }}

    Administer any user on the MAS authentication server

  • - {% elif scope is matching("^urn:matrix:org.matrix.msc2967.client:device:") %} +
  • {{ icon.error() }}

    Administer any user on the MAS authentication server

  • + {% elif scope is starting_with("urn:matrix:org.matrix.msc2967.client:device:") %} {# We hide this scope #} {% else %} -
  • {{ icon::info() }}

    {{ scope }}

  • +
  • {{ icon.info() }}

    {{ scope }}

  • {% endif %} {% endfor %}
diff --git a/templates/form_post.html b/templates/form_post.html index 2ecb43ef..792ad882 100644 --- a/templates/form_post.html +++ b/templates/form_post.html @@ -23,7 +23,7 @@ limitations under the License.
- {% for key, value in params %} + {% for key, value in params|items %} {% endfor %}
diff --git a/templates/pages/account/emails/add.html b/templates/pages/account/emails/add.html index ca0c4304..f5bd3e25 100644 --- a/templates/pages/account/emails/add.html +++ b/templates/pages/account/emails/add.html @@ -17,7 +17,7 @@ limitations under the License. {% extends "base.html" %} {% block content %} - {{ navbar::top() }} + {{ navbar.top() }}
@@ -27,13 +27,13 @@ limitations under the License. {% if form.errors is not empty %} {% for error in form.errors %}
- {{ errors::form_error_message(error=error) }} + {{ errors.form_error_message(error=error) }}
{% endfor %} {% endif %} - {{ field::input(label="Email", name="email", type="email", form_state=form, autocomplete="email", required=true) }} - {{ button::button(text="Next") }} + {{ field.input(label="Email", name="email", type="email", form_state=form, autocomplete="email", required=true) }} + {{ button.button(text="Next") }}
{% endblock content %} diff --git a/templates/pages/account/emails/verify.html b/templates/pages/account/emails/verify.html index 5a7f1b31..8ec3efff 100644 --- a/templates/pages/account/emails/verify.html +++ b/templates/pages/account/emails/verify.html @@ -17,7 +17,7 @@ limitations under the License. {% extends "base.html" %} {% block content %} - {{ navbar::top() }} + {{ navbar.top() }}
@@ -28,13 +28,13 @@ limitations under the License. {% if form.errors is not empty %} {% for error in form.errors %}
- {{ errors::form_error_message(error=error) }} + {{ errors.form_error_message(error=error) }}
{% endfor %} {% endif %} - {{ field::input(label="Code", name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }} - {{ button::button(text="Submit") }} + {{ field.input(label="Code", name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }} + {{ button.button(text="Submit") }}
{% endblock content %} diff --git a/templates/pages/account/password.html b/templates/pages/account/password.html index a26d0b93..6f410630 100644 --- a/templates/pages/account/password.html +++ b/templates/pages/account/password.html @@ -17,15 +17,15 @@ limitations under the License. {% extends "base.html" %} {% block content %} - {{ navbar::top() }} + {{ navbar.top() }}

Change my password

- {{ 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="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") }} + {{ 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="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") }}
{% endblock content %} diff --git a/templates/pages/consent.html b/templates/pages/consent.html index ebe7f722..959789d5 100644 --- a/templates/pages/consent.html +++ b/templates/pages/consent.html @@ -17,7 +17,7 @@ limitations under the License. {% extends "base.html" %} {% block content %} - {% set client_name = client.client_name | default(value=client.client_id) %} + {% set client_name = client.client_name | default(client.client_id) %}
@@ -25,7 +25,7 @@ limitations under the License. {% else %} {% endif %} @@ -34,7 +34,7 @@ limitations under the License.
@@ -56,10 +56,10 @@ limitations under the License.
- {{ button::button(text="Continue") }} + {{ button.button(text="Continue") }}
- {{ back_to_client::link( + {{ back_to_client.link( text="Cancel", kind="tertiary", uri=grant.redirect_uri, @@ -69,7 +69,7 @@ limitations under the License.
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) }}
diff --git a/templates/pages/index.html b/templates/pages/index.html index bc0dfc5c..f973b476 100644 --- a/templates/pages/index.html +++ b/templates/pages/index.html @@ -17,7 +17,7 @@ limitations under the License. {% extends "base.html" %} {% block content %} - {{ navbar::top() }} + {{ navbar.top() }}

Matrix Authentication Service

diff --git a/templates/pages/login.html b/templates/pages/login.html index 645c215f..44aef92f 100644 --- a/templates/pages/login.html +++ b/templates/pages/login.html @@ -35,36 +35,36 @@ limitations under the License. {% if form.errors is not empty %} {% for error in form.errors %}
- {{ errors::form_error_message(error=error) }} + {{ errors.form_error_message(error=error) }}
{% endfor %} {% endif %} - {{ 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="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") }} {% if next and next.kind == "continue_authorization_grant" %}
- {{ back_to_client::link( + {{ back_to_client.link( text="Cancel", kind="destructive", uri=next.grant.redirect_uri, mode=next.grant.response_mode, params=dict(error="access_denied", state=next.grant.state) ) }} - {{ button::button(text="Next") }} + {{ button.button(text="Next") }}
{% else %}
- {{ button::button(text="Next") }} + {{ button.button(text="Next") }}
{% endif %} {% if not next or next.kind != "link_upstream" %}
Don't have an account yet? - {% set params = next | safe_get(key="params") | to_params(prefix="?") %} - {{ button::link_text(text="Create an account", href="/register" ~ params) }} + {% set params = next["params"] | default({}) | to_params(prefix="?") %} + {{ button.link_text(text="Create an account", href="/register" ~ params) }}
{% endif %} {% endif %} @@ -79,8 +79,8 @@ limitations under the License. {% endif %} {% for provider in providers %} - {% set params = next | safe_get(key="params") | to_params(prefix="?") %} - {{ button::link(text="Continue with " ~ provider.issuer, href="/upstream/authorize/" ~ provider.id ~ params) }} + {% set params = next["params"] | default({}) | to_params(prefix="?") %} + {{ button.link(text="Continue with " ~ provider.issuer, href="/upstream/authorize/" ~ provider.id ~ params) }} {% endfor %} {% endif %} diff --git a/templates/pages/policy_violation.html b/templates/pages/policy_violation.html index d23d3c8e..1747eb18 100644 --- a/templates/pages/policy_violation.html +++ b/templates/pages/policy_violation.html @@ -28,7 +28,7 @@ limitations under the License. {% endif %}
-

{{ client.client_name | default(value=client.client_id) }}

+

{{ client.client_name | default(client.client_id) }}

@@ -36,10 +36,10 @@ limitations under the License. Logged as {{ current_session.user.username }}
- {{ 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) }} - {{ back_to_client::link( + {{ back_to_client.link( text="Cancel", kind="destructive", uri=grant.redirect_uri, diff --git a/templates/pages/reauth.html b/templates/pages/reauth.html index 00b5a443..02feb7e9 100644 --- a/templates/pages/reauth.html +++ b/templates/pages/reauth.html @@ -26,28 +26,28 @@ limitations under the License. {# 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" %}
- {{ back_to_client::link( + {{ back_to_client.link( text="Cancel", kind="destructive", uri=next.grant.redirect_uri, mode=next.grant.response_mode, params=dict(error="access_denied", state=next.grant.state) ) }} - {{ button::button(text="Next") }} + {{ button.button(text="Next") }}
{% else %}
- {{ button::button(text="Next") }} + {{ button.button(text="Next") }}
{% endif %}
Not {{ current_session.user.username }}? - {% set post_logout_action = next | safe_get(key="params") %} - {{ logout::button(text="Sign out", csrf_token=csrf_token, post_logout_action=post_logout_action, as_link=true) }} + {% 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) }}
diff --git a/templates/pages/register.html b/templates/pages/register.html index f5d106ce..caa85e3e 100644 --- a/templates/pages/register.html +++ b/templates/pages/register.html @@ -26,37 +26,37 @@ limitations under the License. {% if form.errors is not empty %} {% for error in form.errors %}
- {{ errors::form_error_message(error=error) }} + {{ errors.form_error_message(error=error) }}
{% endfor %} {% endif %} - {{ 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="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="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="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") }} {% if next and next.kind == "continue_authorization_grant" %}
- {{ back_to_client::link( + {{ back_to_client.link( text="Cancel", kind="destructive", uri=next.grant.redirect_uri, mode=next.grant.response_mode, params=dict(error="access_denied", state=next.grant.state) ) }} - {{ button::button(text="Next") }} + {{ button.button(text="Next") }}
{% else %}
- {{ button::button(text="Next") }} + {{ button.button(text="Next") }}
{% endif %}
Already have an account? - {% set params = next | safe_get(key="params") | to_params(prefix="?") %} - {{ button::link_text(text="Sign in instead", href="/login" ~ params) }} + {% set params = next["params"] | default({}) | to_params(prefix="?") %} + {{ button.link_text(text="Sign in instead", href="/login" ~ params) }}
diff --git a/templates/pages/sso.html b/templates/pages/sso.html index 1acf6640..5b291f1a 100644 --- a/templates/pages/sso.html +++ b/templates/pages/sso.html @@ -21,19 +21,15 @@ limitations under the License.
- {% if client.logo_uri %} - - {% else %} - - {% endif %} +

{{ client_name }} wants to access your account. This will allow {{ client_name }} to:

@@ -43,12 +39,12 @@ limitations under the License.
- {{ button::button(text="Continue") }} + {{ button.button(text="Continue") }}
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) }}
diff --git a/templates/pages/upstream_oauth2/do_register.html b/templates/pages/upstream_oauth2/do_register.html index 87f4aa4c..2546c4d7 100644 --- a/templates/pages/upstream_oauth2/do_register.html +++ b/templates/pages/upstream_oauth2/do_register.html @@ -36,7 +36,7 @@ limitations under the License.
{{ suggested_localpart }}
{% 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 %} {% if suggested_email %} @@ -67,14 +67,14 @@ limitations under the License. {% endif %} - {{ button::button(text="Create a new account") }} + {{ button.button(text="Create a new account") }}

Or

- {{ button::link_outline(text="Link to an existing account", href=login_link) }} + {{ button.link_outline(text="Link to an existing account", href=login_link) }} {% endblock content %} diff --git a/templates/pages/upstream_oauth2/link_mismatch.html b/templates/pages/upstream_oauth2/link_mismatch.html index 1e6018d9..a3d81663 100644 --- a/templates/pages/upstream_oauth2/link_mismatch.html +++ b/templates/pages/upstream_oauth2/link_mismatch.html @@ -17,14 +17,14 @@ limitations under the License. {% extends "base.html" %} {% block content %} - {{ navbar::top() }} + {{ navbar.top() }}

This upstream account is already linked to another account.

-
{{ logout::button(text="Logout", csrf_token=csrf_token) }}
+
{{ logout.button(text="Logout", csrf_token=csrf_token) }}
{% endblock content %} diff --git a/templates/pages/upstream_oauth2/suggest_link.html b/templates/pages/upstream_oauth2/suggest_link.html index 3b07b5f1..c39b5e91 100644 --- a/templates/pages/upstream_oauth2/suggest_link.html +++ b/templates/pages/upstream_oauth2/suggest_link.html @@ -17,7 +17,7 @@ limitations under the License. {% extends "base.html" %} {% block content %} - {{ navbar::top() }} + {{ navbar.top() }}

@@ -28,10 +28,10 @@ limitations under the License. - {{ button::button(text="Link", class="flex-1") }} + {{ button.button(text="Link", class="flex-1") }} -
Or {{ logout::button(text="Logout", csrf_token=csrf_token, post_logout_action=post_logout_action, as_link=true) }}
+
Or {{ logout.button(text="Logout", csrf_token=csrf_token, post_logout_action=post_logout_action, as_link=true) }}

{% endblock content %}