1
0
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:
Quentin Gliech
2023-10-02 14:41:07 +02:00
parent 536bf4907b
commit 995bdfc13b
45 changed files with 370 additions and 314 deletions

25
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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(())
}

View File

@@ -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<Message, Error> {
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(())
}

View File

@@ -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());
}

View File

@@ -409,7 +409,7 @@ where
// Error responses should have an ErrorContext attached to them
let ext = response.extensions().get::<ErrorContext>();
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)))
}

View File

@@ -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())
}
}

View File

@@ -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())
}

View File

@@ -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) => {

View File

@@ -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())
}

View File

@@ -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()
}
};

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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");

View File

@@ -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)
}

View File

@@ -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())
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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<bool, tera::Error> {
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<String, Value>) -> Result<Value, tera::Error> {
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<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() {
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<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()),
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<String, Value>) -> Result<Value, tera::Error> {
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<String, Value>) -> Result<V
// Do nothing else for non-HTTPS URLs
if url.scheme() != "https" {
return Ok(Value::String(url.to_string()));
return url.to_string();
}
// Only return the domain name
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 {
@@ -118,29 +123,25 @@ enum ParamsWhere {
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};
// 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<String, Value>) -> Result<Value,
let existing: HashMap<String, Value> = 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<String, Value>) -> Result<Value,
uri
};
Ok(Value::String(uri.to_string()))
Ok(uri.to_string())
}
/*
fn function_merge(params: &HashMap<String, Value>) -> Result<Value, tera::Error> {
let mut ret = serde_json::Map::new();
for (k, v) in params {
@@ -182,50 +195,41 @@ fn function_merge(params: &HashMap<String, Value>) -> Result<Value, tera::Error>
Ok(Value::Object(ret))
}
*/
#[allow(clippy::unnecessary_wraps)]
fn function_dict(params: &HashMap<String, Value>) -> Result<Value, tera::Error> {
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<String, Value>) -> tera::Result<Value> {
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<Value, Error> {
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")))
}
}

View File

@@ -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<RwLock<Tera>>,
environment: Arc<ArcSwap<minijinja::Environment<'static>>>,
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<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 {
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<Tera, TemplateLoadingError> {
) -> Result<minijinja::Environment<'static>, 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<chrono::Utc>,
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::<EmptyContext>(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::<EmptyContext>(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();
}
}

View File

@@ -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<String, TemplateError> {
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<chrono::Utc>, 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))?;
}

View File

@@ -23,7 +23,7 @@ limitations under the License.
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>matrix-authentication-service</title>
<script>
window.APP_CONFIG = JSON.parse("{{ app_config | json_encode | addslashes | safe }}");
window.APP_CONFIG = JSON.parse("{{ app_config | tojson | add_slashes | safe }}");
(function () {
const query = window.matchMedia("(prefers-color-scheme: dark)");
function handleChange(list) {
@@ -38,7 +38,7 @@ limitations under the License.
handleChange(query);
})();
</script>
{{ include_asset(path='src/main.tsx', preload=true) | indent(prefix=" ") | safe }}
{{ include_asset('src/main.tsx', preload=true) | indent(4) | safe }}
</head>
<body>

View File

@@ -29,7 +29,7 @@ limitations under the License.
<meta charset="utf-8">
<title>{% block title %}matrix-authentication-service{% endblock title %}</title>
<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>
<body class="flex flex-col min-h-screen">
{% block content %}{% endblock content %}

View File

@@ -25,13 +25,13 @@ limitations under the License.
{% if mode == "form_post" %}
<form method="post" action="{{ uri }}">
{% for key, value in params %}
{% for key, value in params|items %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
<button class="{{ class }}" data-kind="{{ kind }}" data-size="lg" type="submit">{{ text }}</button>
</form>
{% 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 %}
{{ throw(message="Invalid mode") }}
{% endif %}

View File

@@ -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": ""}) %}
<div class="flex flex-col cpd-field {{ class }}">
<div class="cpd-label"{% if state.errors is not empty %} data-invalid{% endif %}>{{ label }}</div>
@@ -53,4 +53,4 @@ limitations under the License.
{% endfor %}
{% endif %}
</div>
{% endmacro input %}
{% endmacro %}

View File

@@ -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={}) %}
<form method="POST" action="/logout" class="inline">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{% if post_logout_action %}
{% for key, value in post_logout_action %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
{% endif %}
{% for key, value in post_logout_action|items %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
<button class="{% if as_link %}cpd-link{% else %}cpd-button{% endif %}" data-kind="critical" type="submit">{{ text }}</button>
</form>
{% endmacro %}

View File

@@ -24,12 +24,12 @@ limitations under the License.
Signed in as <span class="font-semibold">{{ current_session.user.username }}</span>.
</div>
{{ 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 %}
</div>
</nav>
{% endmacro top %}
{% endmacro %}

View File

@@ -16,23 +16,23 @@ limitations under the License.
{% macro list(scopes) %}
<ul>
{% for scope in scopes | split(pat=" ") %}
{% for scope in (scopes | split(" ")) %}
{% 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:*" %}
<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.info() }}<p>Edit your profile and contact details</p></li>
<li>{{ icon.computer() }}<p>Manage your devices and sessions</p></li>
{% elif scope == "urn:matrix:org.matrix.msc2967.client:api:*" %}
<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.chat() }}<p>View your existing messages and data</p></li>
<li>{{ icon.check_circle() }}<p>Send new messages on your behalf</p></li>
{% 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" %}
<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:") %}
<li>{{ icon.error() }}<p>Administer any user on the MAS authentication server</p></li>
{% elif scope is starting_with("urn:matrix:org.matrix.msc2967.client:device:") %}
{# We hide this scope #}
{% else %}
<li>{{ icon::info() }}<p>{{ scope }}</p></li>
<li>{{ icon.info() }}<p>{{ scope }}</p></li>
{% endif %}
{% endfor %}
</ul>

View File

@@ -23,7 +23,7 @@ limitations under the License.
</head>
<body onload="javascript:document.forms[0].submit()">
<form method="post" action="{{ redirect_uri }}">
{% for key, value in params %}
{% for key, value in params|items %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
</form>

View File

@@ -17,7 +17,7 @@ limitations under the License.
{% extends "base.html" %}
{% block content %}
{{ navbar::top() }}
{{ navbar.top() }}
<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">
<div class="text-center">
@@ -27,13 +27,13 @@ limitations under the License.
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-critical font-medium">
{{ errors::form_error_message(error=error) }}
{{ errors.form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ 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") }}
</section>
{% endblock content %}

View File

@@ -17,7 +17,7 @@ limitations under the License.
{% extends "base.html" %}
{% block content %}
{{ navbar::top() }}
{{ navbar.top() }}
<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">
<div class="text-center">
@@ -28,13 +28,13 @@ limitations under the License.
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-critical font-medium">
{{ errors::form_error_message(error=error) }}
{{ errors.form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ 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") }}
</section>
{% endblock content %}

View File

@@ -17,15 +17,15 @@ limitations under the License.
{% extends "base.html" %}
{% 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">
<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>
<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="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") }}
</form>
</section>
{% endblock content %}

View File

@@ -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) %}
<section class="flex items-center justify-center flex-1">
<div class="w-96 mx-2 my-8 flex flex-col gap-6">
<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 }}" />
{% else %}
<div class="consent-client-icon generic">
{{ icon::web_browser() }}
{{ icon.web_browser() }}
</div>
{% endif %}
@@ -34,7 +34,7 @@ limitations under the License.
</div>
<div class="consent-scope-list">
{{ scope::list(scopes=grant.scope) }}
{{ scope.list(scopes=grant.scope) }}
</div>
<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">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ button::button(text="Continue") }}
{{ button.button(text="Continue") }}
</form>
{{ back_to_client::link(
{{ back_to_client.link(
text="Cancel",
kind="tertiary",
uri=grant.redirect_uri,
@@ -69,7 +69,7 @@ limitations under the License.
<div class="text-center">
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>
</section>

View File

@@ -17,7 +17,7 @@ limitations under the License.
{% extends "base.html" %}
{% block content %}
{{ navbar::top() }}
{{ navbar.top() }}
<section class="flex-1 flex flex-col items-center justify-center">
<div class="my-2 mx-8">
<h1 class="my-2 text-5xl font-semibold leading-tight">Matrix Authentication Service</h1>

View File

@@ -35,36 +35,36 @@ limitations under the License.
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-critical font-medium">
{{ errors::form_error_message(error=error) }}
{{ errors.form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<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="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" %}
<div class="grid grid-cols-2 gap-4">
{{ 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") }}
</div>
{% else %}
<div class="grid grid-cols-1 gap-4">
{{ button::button(text="Next") }}
{{ button.button(text="Next") }}
</div>
{% endif %}
{% if not next or next.kind != "link_upstream" %}
<div class="text-center mt-4">
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) }}
</div>
{% 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 %}

View File

@@ -28,7 +28,7 @@ limitations under the License.
<img referrerpolicy="no-referrer" class="w-16 h-16" src="{{ client.logo_uri }}" />
{% endif %}
</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 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>
</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>
{{ back_to_client::link(
{{ back_to_client.link(
text="Cancel",
kind="destructive",
uri=grant.redirect_uri,

View File

@@ -26,28 +26,28 @@ limitations under the License.
</div>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{# 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" %}
<div class="grid grid-cols-2 gap-4">
{{ 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") }}
</div>
{% else %}
<div class="grid grid-cols-1 gap-4">
{{ button::button(text="Next") }}
{{ button.button(text="Next") }}
</div>
{% endif %}
</form>
<div class="text-center mt-4">
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) }}
</div>
</div>
</section>

View File

@@ -26,37 +26,37 @@ limitations under the License.
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-critical font-medium">
{{ errors::form_error_message(error=error) }}
{{ errors.form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<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="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" %}
<div class="grid grid-cols-2 gap-4">
{{ 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") }}
</div>
{% else %}
<div class="grid grid-cols-1 gap-4">
{{ button::button(text="Next") }}
{{ button.button(text="Next") }}
</div>
{% endif %}
<div class="text-center mt-4">
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) }}
</div>
</form>
</section>

View File

@@ -21,19 +21,15 @@ limitations under the License.
<section class="flex items-center justify-center flex-1">
<div class="w-96 mx-2 my-8 flex flex-col gap-6">
<div class="flex flex-col gap-2 text-center">
{% if client.logo_uri %}
<img class="consent-client-icon image" referrerpolicy="no-referrer" src="{{ client.logo_uri }}" />
{% else %}
<div class="consent-client-icon generic">
{{ icon::web_browser() }}
</div>
{% endif %}
<div class="consent-client-icon generic">
{{ icon.web_browser() }}
</div>
<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 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 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">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ button::button(text="Continue") }}
{{ button.button(text="Continue") }}
</form>
<div class="text-center">
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>
</section>

View File

@@ -36,7 +36,7 @@ limitations under the License.
<div class="font-mono">{{ suggested_localpart }}</div>
</div>
{% 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.
</div>
{% endif %}
{{ button::button(text="Create a new account") }}
{{ button.button(text="Create a new account") }}
</form>
<div class="flex items-center">
<hr class="flex-1" />
<div class="mx-2">Or</div>
<hr class="flex-1" />
</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>
</section>
{% endblock content %}

View File

@@ -17,14 +17,14 @@ limitations under the License.
{% extends "base.html" %}
{% block content %}
{{ navbar::top() }}
{{ navbar.top() }}
<section class="flex items-center justify-center flex-1">
<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">
This upstream account is already linked to another account.
</h1>
<div>{{ logout::button(text="Logout", csrf_token=csrf_token) }}</div>
<div>{{ logout.button(text="Logout", csrf_token=csrf_token) }}</div>
</div>
</section>
{% endblock content %}

View File

@@ -17,7 +17,7 @@ limitations under the License.
{% extends "base.html" %}
{% block content %}
{{ navbar::top() }}
{{ navbar.top() }}
<section class="flex items-center justify-center flex-1">
<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">
@@ -28,10 +28,10 @@ limitations under the License.
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<input type="hidden" name="action" value="link" />
{{ button::button(text="Link", class="flex-1") }}
{{ button.button(text="Link", class="flex-1") }}
</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>
</section>
{% endblock content %}