1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-07 17:03:01 +03:00

handlers/templates: infer the language from the Accept-Language browser header

This commit is contained in:
Quentin Gliech
2023-10-04 18:13:57 +02:00
parent 873651a780
commit 1feafc1d13
25 changed files with 253 additions and 76 deletions

2
Cargo.lock generated
View File

@@ -2740,6 +2740,7 @@ dependencies = [
"mas-graphql", "mas-graphql",
"mas-handlers", "mas-handlers",
"mas-http", "mas-http",
"mas-i18n",
"mas-iana", "mas-iana",
"mas-keystore", "mas-keystore",
"mas-listener", "mas-listener",
@@ -2887,6 +2888,7 @@ dependencies = [
"mas-data-model", "mas-data-model",
"mas-graphql", "mas-graphql",
"mas-http", "mas-http",
"mas-i18n",
"mas-iana", "mas-iana",
"mas-jose", "mas-jose",
"mas-keystore", "mas-keystore",

View File

@@ -50,6 +50,13 @@ pub struct AcceptLanguage {
parts: Vec<AcceptLanguagePart>, parts: Vec<AcceptLanguagePart>,
} }
impl AcceptLanguage {
pub fn iter(&self) -> impl Iterator<Item = &Locale> {
// This should stop when we hit the first None, aka the first *
self.parts.iter().map_while(|item| item.locale.as_ref())
}
}
/// Utility to trim ASCII whitespace from the start and end of a byte slice /// Utility to trim ASCII whitespace from the start and end of a byte slice
const fn trim_bytes(mut bytes: &[u8]) -> &[u8] { const fn trim_bytes(mut bytes: &[u8]) -> &[u8] {
// Trim leading and trailing whitespace // Trim leading and trailing whitespace

View File

@@ -53,6 +53,7 @@ mas-email = { path = "../email" }
mas-graphql = { path = "../graphql" } mas-graphql = { path = "../graphql" }
mas-handlers = { path = "../handlers", default-features = false } mas-handlers = { path = "../handlers", default-features = false }
mas-http = { path = "../http", default-features = false, features = ["axum", "client"] } mas-http = { path = "../http", default-features = false, features = ["axum", "client"] }
mas-i18n = { path = "../i18n" }
mas-iana = { path = "../iana" } mas-iana = { path = "../iana" }
mas-keystore = { path = "../keystore" } mas-keystore = { path = "../keystore" }
mas-listener = { path = "../listener" } mas-listener = { path = "../listener" }

View File

@@ -23,6 +23,7 @@ use mas_handlers::{
passwords::PasswordManager, ActivityTracker, BoundActivityTracker, CookieManager, ErrorWrapper, passwords::PasswordManager, ActivityTracker, BoundActivityTracker, CookieManager, ErrorWrapper,
HttpClientFactory, MatrixHomeserver, MetadataCache, SiteConfig, HttpClientFactory, MatrixHomeserver, MetadataCache, SiteConfig,
}; };
use mas_i18n::Translator;
use mas_keystore::{Encrypter, Keystore}; use mas_keystore::{Encrypter, Keystore};
use mas_policy::{Policy, PolicyFactory}; use mas_policy::{Policy, PolicyFactory};
use mas_router::UrlBuilder; use mas_router::UrlBuilder;
@@ -152,6 +153,12 @@ impl FromRef<AppState> for Templates {
} }
} }
impl FromRef<AppState> for Arc<Translator> {
fn from_ref(input: &AppState) -> Self {
input.templates.translator()
}
}
impl FromRef<AppState> for Keystore { impl FromRef<AppState> for Keystore {
fn from_ref(input: &AppState) -> Self { fn from_ref(input: &AppState) -> Self {
input.key_store.clone() input.key_store.clone()

View File

@@ -66,6 +66,7 @@ mas-axum-utils = { path = "../axum-utils", default-features = false }
mas-data-model = { path = "../data-model" } mas-data-model = { path = "../data-model" }
mas-graphql = { path = "../graphql" } mas-graphql = { path = "../graphql" }
mas-http = { path = "../http", default-features = false } mas-http = { path = "../http", default-features = false }
mas-i18n = { path = "../i18n" }
mas-iana = { path = "../iana" } mas-iana = { path = "../iana" }
mas-jose = { path = "../jose" } mas-jose = { path = "../jose" }
mas-keystore = { path = "../keystore" } mas-keystore = { path = "../keystore" }

View File

@@ -37,6 +37,8 @@ use mas_templates::{CompatSsoContext, ErrorContext, TemplateContext, Templates};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ulid::Ulid; use ulid::Ulid;
use crate::PreferredLanguage;
#[derive(Serialize)] #[derive(Serialize)]
struct AllParams<'s> { struct AllParams<'s> {
#[serde(flatten)] #[serde(flatten)]
@@ -58,6 +60,7 @@ pub struct Params {
err, err,
)] )]
pub async fn get( pub async fn get(
PreferredLanguage(locale): PreferredLanguage,
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
@@ -110,7 +113,8 @@ pub async fn get(
let ctx = CompatSsoContext::new(login) let ctx = CompatSsoContext::new(login)
.with_session(session) .with_session(session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_sso_login(&ctx)?; let content = templates.render_sso_login(&ctx)?;

View File

@@ -53,7 +53,7 @@ use mas_keystore::{Encrypter, Keystore};
use mas_policy::Policy; use mas_policy::Policy;
use mas_router::{Route, UrlBuilder}; use mas_router::{Route, UrlBuilder};
use mas_storage::{BoxClock, BoxRepository, BoxRng}; use mas_storage::{BoxClock, BoxRepository, BoxRng};
use mas_templates::{ErrorContext, NotFoundContext, Templates}; use mas_templates::{ErrorContext, NotFoundContext, TemplateContext, Templates};
use passwords::PasswordManager; use passwords::PasswordManager;
use sqlx::PgPool; use sqlx::PgPool;
use tower::util::AndThenLayer; use tower::util::AndThenLayer;
@@ -68,6 +68,7 @@ pub mod upstream_oauth2;
mod views; mod views;
mod activity_tracker; mod activity_tracker;
mod preferred_language;
mod site_config; mod site_config;
#[cfg(test)] #[cfg(test)]
mod test_utils; mod test_utils;
@@ -96,6 +97,7 @@ pub use self::{
activity_tracker::{ActivityTracker, Bound as BoundActivityTracker}, activity_tracker::{ActivityTracker, Bound as BoundActivityTracker},
compat::MatrixHomeserver, compat::MatrixHomeserver,
graphql::schema as graphql_schema, graphql::schema as graphql_schema,
preferred_language::PreferredLanguage,
site_config::SiteConfig, site_config::SiteConfig,
upstream_oauth2::cache::MetadataCache, upstream_oauth2::cache::MetadataCache,
}; };
@@ -298,6 +300,7 @@ where
<B as HttpBody>::Error: std::error::Error + Send + Sync, <B as HttpBody>::Error: std::error::Error + Send + Sync,
S: Clone + Send + Sync + 'static, S: Clone + Send + Sync + 'static,
UrlBuilder: FromRef<S>, UrlBuilder: FromRef<S>,
PreferredLanguage: FromRequestParts<S>,
BoxRepository: FromRequestParts<S>, BoxRepository: FromRequestParts<S>,
CookieJar: FromRequestParts<S>, CookieJar: FromRequestParts<S>,
BoundActivityTracker: FromRequestParts<S>, BoundActivityTracker: FromRequestParts<S>,
@@ -433,8 +436,9 @@ pub async fn fallback(
OriginalUri(uri): OriginalUri, OriginalUri(uri): OriginalUri,
method: Method, method: Method,
version: Version, version: Version,
PreferredLanguage(locale): PreferredLanguage,
) -> Result<impl IntoResponse, FancyError> { ) -> Result<impl IntoResponse, FancyError> {
let ctx = NotFoundContext::new(&method, version, &uri); let ctx = NotFoundContext::new(&method, version, &uri).with_language(locale);
// XXX: this should look at the Accept header and return JSON if requested // XXX: this should look at the Accept header and return JSON if requested
let res = templates.render_not_found(&ctx)?; let res = templates.render_not_found(&ctx)?;

View File

@@ -34,7 +34,9 @@ use tracing::warn;
use ulid::Ulid; use ulid::Ulid;
use super::callback::CallbackDestination; use super::callback::CallbackDestination;
use crate::{impl_from_error_for_route, oauth2::generate_id_token, BoundActivityTracker}; use crate::{
impl_from_error_for_route, oauth2::generate_id_token, BoundActivityTracker, PreferredLanguage,
};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum RouteError { pub enum RouteError {
@@ -89,6 +91,7 @@ impl_from_error_for_route!(super::callback::CallbackDestinationError);
pub(crate) async fn get( pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>, State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>, State(url_builder): State<UrlBuilder>,
State(key_store): State<Keystore>, State(key_store): State<Keystore>,
@@ -160,7 +163,8 @@ pub(crate) async fn get(
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let ctx = PolicyViolationContext::new(grant, client) let ctx = PolicyViolationContext::new(grant, client)
.with_session(session) .with_session(session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_policy_violation(&ctx)?; let content = templates.render_policy_violation(&ctx)?;

View File

@@ -39,7 +39,7 @@ use thiserror::Error;
use tracing::warn; use tracing::warn;
use self::{callback::CallbackDestination, complete::GrantCompletionError}; use self::{callback::CallbackDestination, complete::GrantCompletionError};
use crate::{impl_from_error_for_route, BoundActivityTracker}; use crate::{impl_from_error_for_route, BoundActivityTracker, PreferredLanguage};
mod callback; mod callback;
pub mod complete; pub mod complete;
@@ -139,6 +139,7 @@ fn resolve_response_mode(
pub(crate) async fn get( pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>, State(templates): State<Templates>,
State(key_store): State<Keystore>, State(key_store): State<Keystore>,
State(url_builder): State<UrlBuilder>, State(url_builder): State<UrlBuilder>,
@@ -418,7 +419,8 @@ pub(crate) async fn get(
let ctx = PolicyViolationContext::new(grant, client) let ctx = PolicyViolationContext::new(grant, client)
.with_session(user_session) .with_session(user_session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_policy_violation(&ctx)?; let content = templates.render_policy_violation(&ctx)?;
Html(content).into_response() Html(content).into_response()

View File

@@ -34,7 +34,7 @@ use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Tem
use thiserror::Error; use thiserror::Error;
use ulid::Ulid; use ulid::Ulid;
use crate::{impl_from_error_for_route, BoundActivityTracker}; use crate::{impl_from_error_for_route, BoundActivityTracker, PreferredLanguage};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum RouteError { pub enum RouteError {
@@ -82,6 +82,7 @@ impl IntoResponse for RouteError {
pub(crate) async fn get( pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>, State(templates): State<Templates>,
mut policy: Policy, mut policy: Policy,
mut repo: BoxRepository, mut repo: BoxRepository,
@@ -123,7 +124,8 @@ pub(crate) async fn get(
if res.valid() { if res.valid() {
let ctx = ConsentContext::new(grant, client) let ctx = ConsentContext::new(grant, client)
.with_session(session) .with_session(session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_consent(&ctx)?; let content = templates.render_consent(&ctx)?;
@@ -131,7 +133,8 @@ pub(crate) async fn get(
} else { } else {
let ctx = PolicyViolationContext::new(grant, client) let ctx = PolicyViolationContext::new(grant, client)
.with_session(session) .with_session(session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_policy_violation(&ctx)?; let content = templates.render_policy_violation(&ctx)?;

View File

@@ -0,0 +1,53 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{convert::Infallible, sync::Arc};
use axum::{
async_trait,
extract::{FromRef, FromRequestParts},
http::request::Parts,
TypedHeader,
};
use mas_axum_utils::language_detection::AcceptLanguage;
use mas_i18n::{DataLocale, Translator};
pub struct PreferredLanguage(pub DataLocale);
#[async_trait]
impl<S> FromRequestParts<S> for PreferredLanguage
where
S: Send + Sync,
Arc<Translator>: FromRef<S>,
{
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let translator: Arc<Translator> = FromRef::from_ref(state);
let accept_language: Option<TypedHeader<AcceptLanguage>> =
FromRequestParts::from_request_parts(parts, state).await?;
let supported_language = translator.available_locales();
let locale = accept_language
.and_then(|TypedHeader(accept_language)| {
accept_language.iter().find_map(|lang| {
let locale: DataLocale = lang.into();
supported_language.contains(&&locale).then_some(locale)
})
})
.unwrap_or("en".parse().unwrap());
Ok(PreferredLanguage(locale))
}
}

View File

@@ -33,6 +33,7 @@ use hyper::{
use mas_axum_utils::{ use mas_axum_utils::{
cookies::CookieManager, http_client_factory::HttpClientFactory, ErrorWrapper, cookies::CookieManager, http_client_factory::HttpClientFactory, ErrorWrapper,
}; };
use mas_i18n::Translator;
use mas_keystore::{Encrypter, JsonWebKey, JsonWebKeySet, Keystore, PrivateKey}; use mas_keystore::{Encrypter, JsonWebKey, JsonWebKeySet, Keystore, PrivateKey};
use mas_matrix::{HomeserverConnection, MockHomeserverConnection}; use mas_matrix::{HomeserverConnection, MockHomeserverConnection};
use mas_policy::{InstantiateError, Policy, PolicyFactory}; use mas_policy::{InstantiateError, Policy, PolicyFactory};
@@ -319,6 +320,12 @@ impl FromRef<TestState> for Templates {
} }
} }
impl FromRef<TestState> for Arc<Translator> {
fn from_ref(input: &TestState) -> Self {
input.templates.translator()
}
}
impl FromRef<TestState> for Keystore { impl FromRef<TestState> for Keystore {
fn from_ref(input: &TestState) -> Self { fn from_ref(input: &TestState) -> Self {
input.key_store.clone() input.key_store.clone()

View File

@@ -42,7 +42,7 @@ use thiserror::Error;
use ulid::Ulid; use ulid::Ulid;
use super::UpstreamSessionsCookie; use super::UpstreamSessionsCookie;
use crate::{impl_from_error_for_route, views::shared::OptionalPostAuthAction}; use crate::{impl_from_error_for_route, views::shared::OptionalPostAuthAction, PreferredLanguage};
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub(crate) enum RouteError { pub(crate) enum RouteError {
@@ -189,6 +189,7 @@ pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
mut repo: BoxRepository, mut repo: BoxRepository,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>, State(templates): State<Templates>,
cookie_jar: CookieJar, cookie_jar: CookieJar,
user_agent: Option<TypedHeader<headers::UserAgent>>, user_agent: Option<TypedHeader<headers::UserAgent>>,
@@ -264,7 +265,8 @@ pub(crate) async fn get(
let ctx = UpstreamExistingLinkContext::new(user) let ctx = UpstreamExistingLinkContext::new(user)
.with_session(user_session) .with_session(user_session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
Html(templates.render_upstream_oauth2_link_mismatch(&ctx)?).into_response() Html(templates.render_upstream_oauth2_link_mismatch(&ctx)?).into_response()
} }
@@ -273,7 +275,8 @@ pub(crate) async fn get(
// Session not linked, but user logged in: suggest linking account // Session not linked, but user logged in: suggest linking account
let ctx = UpstreamSuggestLink::new(&link) let ctx = UpstreamSuggestLink::new(&link)
.with_session(user_session) .with_session(user_session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
Html(templates.render_upstream_oauth2_suggest_link(&ctx)?).into_response() Html(templates.render_upstream_oauth2_suggest_link(&ctx)?).into_response()
} }
@@ -358,7 +361,7 @@ pub(crate) async fn get(
}, },
)?; )?;
let ctx = ctx.with_csrf(csrf_token.form_value()); let ctx = ctx.with_csrf(csrf_token.form_value()).with_language(locale);
Html(templates.render_upstream_oauth2_do_register(&ctx)?).into_response() Html(templates.render_upstream_oauth2_do_register(&ctx)?).into_response()
} }

View File

@@ -31,7 +31,7 @@ use mas_storage::{
use mas_templates::{EmailAddContext, ErrorContext, TemplateContext, Templates}; use mas_templates::{EmailAddContext, ErrorContext, TemplateContext, Templates};
use serde::Deserialize; use serde::Deserialize;
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker}; use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker, PreferredLanguage};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct EmailForm { pub struct EmailForm {
@@ -42,6 +42,7 @@ pub struct EmailForm {
pub(crate) async fn get( pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>, State(templates): State<Templates>,
activity_tracker: BoundActivityTracker, activity_tracker: BoundActivityTracker,
mut repo: BoxRepository, mut repo: BoxRepository,
@@ -63,7 +64,8 @@ pub(crate) async fn get(
let ctx = EmailAddContext::new() let ctx = EmailAddContext::new()
.with_session(session) .with_session(session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_account_add_email(&ctx)?; let content = templates.render_account_add_email(&ctx)?;

View File

@@ -32,7 +32,7 @@ use mas_templates::{EmailVerificationPageContext, TemplateContext, Templates};
use serde::Deserialize; use serde::Deserialize;
use ulid::Ulid; use ulid::Ulid;
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker}; use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker, PreferredLanguage};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct CodeForm { pub struct CodeForm {
@@ -48,6 +48,7 @@ pub struct CodeForm {
pub(crate) async fn get( pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>, State(templates): State<Templates>,
activity_tracker: BoundActivityTracker, activity_tracker: BoundActivityTracker,
mut repo: BoxRepository, mut repo: BoxRepository,
@@ -84,7 +85,8 @@ pub(crate) async fn get(
let ctx = EmailVerificationPageContext::new(user_email) let ctx = EmailVerificationPageContext::new(user_email)
.with_session(session) .with_session(session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_account_verify_email(&ctx)?; let content = templates.render_account_verify_email(&ctx)?;

View File

@@ -24,6 +24,7 @@ use mas_axum_utils::{
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_data_model::BrowserSession; use mas_data_model::BrowserSession;
use mas_i18n::DataLocale;
use mas_policy::Policy; use mas_policy::Policy;
use mas_router::Route; use mas_router::Route;
use mas_storage::{ use mas_storage::{
@@ -35,7 +36,7 @@ use rand::Rng;
use serde::Deserialize; use serde::Deserialize;
use zeroize::Zeroizing; use zeroize::Zeroizing;
use crate::{passwords::PasswordManager, BoundActivityTracker}; use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ChangeForm { pub struct ChangeForm {
@@ -48,6 +49,7 @@ pub struct ChangeForm {
pub(crate) async fn get( pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>, State(templates): State<Templates>,
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
activity_tracker: BoundActivityTracker, activity_tracker: BoundActivityTracker,
@@ -68,7 +70,7 @@ pub(crate) async fn get(
.record_browser_session(&clock, &session) .record_browser_session(&clock, &session)
.await; .await;
render(&mut rng, &clock, templates, session, cookie_jar).await render(&mut rng, &clock, locale, templates, session, cookie_jar).await
} else { } else {
let login = mas_router::Login::and_then(mas_router::PostAuthAction::ChangePassword); let login = mas_router::Login::and_then(mas_router::PostAuthAction::ChangePassword);
Ok((cookie_jar, login.go()).into_response()) Ok((cookie_jar, login.go()).into_response())
@@ -78,6 +80,7 @@ pub(crate) async fn get(
async fn render( async fn render(
rng: impl Rng + Send, rng: impl Rng + Send,
clock: &impl Clock, clock: &impl Clock,
locale: DataLocale,
templates: Templates, templates: Templates,
session: BrowserSession, session: BrowserSession,
cookie_jar: CookieJar, cookie_jar: CookieJar,
@@ -86,7 +89,8 @@ async fn render(
let ctx = EmptyContext let ctx = EmptyContext
.with_session(session) .with_session(session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_account_password(&ctx)?; let content = templates.render_account_password(&ctx)?;
@@ -97,6 +101,7 @@ async fn render(
pub(crate) async fn post( pub(crate) async fn post(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
State(templates): State<Templates>, State(templates): State<Templates>,
activity_tracker: BoundActivityTracker, activity_tracker: BoundActivityTracker,
@@ -172,7 +177,15 @@ pub(crate) async fn post(
.record_browser_session(&clock, &session) .record_browser_session(&clock, &session)
.await; .await;
let reply = render(&mut rng, &clock, templates.clone(), session, cookie_jar).await?; let reply = render(
&mut rng,
&clock,
locale,
templates.clone(),
session,
cookie_jar,
)
.await?;
repo.save().await?; repo.save().await?;

View File

@@ -19,12 +19,13 @@ use axum::{
use mas_axum_utils::{cookies::CookieJar, FancyError, SessionInfoExt}; use mas_axum_utils::{cookies::CookieJar, FancyError, SessionInfoExt};
use mas_router::{PostAuthAction, Route}; use mas_router::{PostAuthAction, Route};
use mas_storage::{BoxClock, BoxRepository}; use mas_storage::{BoxClock, BoxRepository};
use mas_templates::{AppContext, Templates}; use mas_templates::{AppContext, TemplateContext, Templates};
use crate::BoundActivityTracker; use crate::{BoundActivityTracker, PreferredLanguage};
#[tracing::instrument(name = "handlers.views.app.get", skip_all, err)] #[tracing::instrument(name = "handlers.views.app.get", skip_all, err)]
pub async fn get( pub async fn get(
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>, State(templates): State<Templates>,
activity_tracker: BoundActivityTracker, activity_tracker: BoundActivityTracker,
action: Option<Query<mas_router::AccountAction>>, action: Option<Query<mas_router::AccountAction>>,
@@ -49,7 +50,7 @@ pub async fn get(
.record_browser_session(&clock, &session) .record_browser_session(&clock, &session)
.await; .await;
let ctx = AppContext::default(); let ctx = AppContext::default().with_language(locale);
let content = templates.render_app(&ctx)?; let content = templates.render_app(&ctx)?;
Ok((cookie_jar, Html(content)).into_response()) Ok((cookie_jar, Html(content)).into_response())

View File

@@ -21,7 +21,7 @@ use mas_router::UrlBuilder;
use mas_storage::{BoxClock, BoxRepository, BoxRng}; use mas_storage::{BoxClock, BoxRepository, BoxRng};
use mas_templates::{IndexContext, TemplateContext, Templates}; use mas_templates::{IndexContext, TemplateContext, Templates};
use crate::BoundActivityTracker; use crate::{preferred_language::PreferredLanguage, BoundActivityTracker};
#[tracing::instrument(name = "handlers.views.index.get", skip_all, err)] #[tracing::instrument(name = "handlers.views.index.get", skip_all, err)]
pub async fn get( pub async fn get(
@@ -32,7 +32,9 @@ pub async fn get(
State(url_builder): State<UrlBuilder>, State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository, mut repo: BoxRepository,
cookie_jar: CookieJar, cookie_jar: CookieJar,
PreferredLanguage(locale): PreferredLanguage,
) -> Result<impl IntoResponse, FancyError> { ) -> Result<impl IntoResponse, FancyError> {
tracing::info!("{locale}");
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info(); let (session_info, cookie_jar) = cookie_jar.session_info();
let session = session_info.load_session(&mut repo).await?; let session = session_info.load_session(&mut repo).await?;
@@ -45,7 +47,8 @@ pub async fn get(
let ctx = IndexContext::new(url_builder.oidc_discovery()) let ctx = IndexContext::new(url_builder.oidc_discovery())
.maybe_with_session(session) .maybe_with_session(session)
.with_csrf(csrf_token.form_value()); .with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_index(&ctx)?; let content = templates.render_index(&ctx)?;

View File

@@ -25,6 +25,7 @@ use mas_axum_utils::{
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_data_model::BrowserSession; use mas_data_model::BrowserSession;
use mas_i18n::DataLocale;
use mas_router::{Route, UpstreamOAuth2Authorize}; use mas_router::{Route, UpstreamOAuth2Authorize};
use mas_storage::{ use mas_storage::{
upstream_oauth2::UpstreamOAuthProviderRepository, upstream_oauth2::UpstreamOAuthProviderRepository,
@@ -39,7 +40,7 @@ use serde::{Deserialize, Serialize};
use zeroize::Zeroizing; use zeroize::Zeroizing;
use super::shared::OptionalPostAuthAction; use super::shared::OptionalPostAuthAction;
use crate::{passwords::PasswordManager, BoundActivityTracker}; use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage};
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub(crate) struct LoginForm { pub(crate) struct LoginForm {
@@ -55,6 +56,7 @@ impl ToFormState for LoginForm {
pub(crate) async fn get( pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
@@ -93,6 +95,7 @@ pub(crate) async fn get(
}; };
let content = render( let content = render(
locale,
LoginContext::default() LoginContext::default()
// XXX: we might want to have a site-wide config in the templates context instead? // XXX: we might want to have a site-wide config in the templates context instead?
.with_password_login(password_manager.is_enabled()) .with_password_login(password_manager.is_enabled())
@@ -111,6 +114,7 @@ pub(crate) async fn get(
pub(crate) async fn post( pub(crate) async fn post(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
State(templates): State<Templates>, State(templates): State<Templates>,
mut repo: BoxRepository, mut repo: BoxRepository,
@@ -148,6 +152,7 @@ pub(crate) async fn post(
if !state.is_valid() { if !state.is_valid() {
let providers = repo.upstream_oauth_provider().all().await?; let providers = repo.upstream_oauth_provider().all().await?;
let content = render( let content = render(
locale,
LoginContext::default() LoginContext::default()
.with_form_state(state) .with_form_state(state)
.with_upstream_providers(providers), .with_upstream_providers(providers),
@@ -187,6 +192,7 @@ pub(crate) async fn post(
let state = state.with_error_on_form(e); let state = state.with_error_on_form(e);
let content = render( let content = render(
locale,
LoginContext::default().with_form_state(state), LoginContext::default().with_form_state(state),
query, query,
csrf_token, csrf_token,
@@ -275,6 +281,7 @@ async fn login(
} }
async fn render( async fn render(
locale: DataLocale,
ctx: LoginContext, ctx: LoginContext,
action: OptionalPostAuthAction, action: OptionalPostAuthAction,
csrf_token: CsrfToken, csrf_token: CsrfToken,
@@ -287,7 +294,7 @@ async fn render(
} else { } else {
ctx ctx
}; };
let ctx = ctx.with_csrf(csrf_token.form_value()); let ctx = ctx.with_csrf(csrf_token.form_value()).with_language(locale);
let content = templates.render_login(&ctx)?; let content = templates.render_login(&ctx)?;
Ok(content) Ok(content)

View File

@@ -33,7 +33,7 @@ use serde::Deserialize;
use zeroize::Zeroizing; use zeroize::Zeroizing;
use super::shared::OptionalPostAuthAction; use super::shared::OptionalPostAuthAction;
use crate::{passwords::PasswordManager, BoundActivityTracker}; use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub(crate) struct ReauthForm { pub(crate) struct ReauthForm {
@@ -44,6 +44,7 @@ pub(crate) struct ReauthForm {
pub(crate) async fn get( pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
State(templates): State<Templates>, State(templates): State<Templates>,
activity_tracker: BoundActivityTracker, activity_tracker: BoundActivityTracker,
@@ -79,7 +80,10 @@ pub(crate) async fn get(
} else { } else {
ctx ctx
}; };
let ctx = ctx.with_session(session).with_csrf(csrf_token.form_value()); let ctx = ctx
.with_session(session)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_reauth(&ctx)?; let content = templates.render_reauth(&ctx)?;

View File

@@ -27,6 +27,7 @@ use mas_axum_utils::{
csrf::{CsrfExt, CsrfToken, ProtectedForm}, csrf::{CsrfExt, CsrfToken, ProtectedForm},
FancyError, SessionInfoExt, FancyError, SessionInfoExt,
}; };
use mas_i18n::DataLocale;
use mas_policy::Policy; use mas_policy::Policy;
use mas_router::Route; use mas_router::Route;
use mas_storage::{ use mas_storage::{
@@ -42,7 +43,7 @@ use serde::{Deserialize, Serialize};
use zeroize::Zeroizing; use zeroize::Zeroizing;
use super::shared::OptionalPostAuthAction; use super::shared::OptionalPostAuthAction;
use crate::{passwords::PasswordManager, BoundActivityTracker}; use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage};
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub(crate) struct RegisterForm { pub(crate) struct RegisterForm {
@@ -60,6 +61,7 @@ impl ToFormState for RegisterForm {
pub(crate) async fn get( pub(crate) async fn get(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>, State(templates): State<Templates>,
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
mut repo: BoxRepository, mut repo: BoxRepository,
@@ -84,6 +86,7 @@ pub(crate) async fn get(
} }
let content = render( let content = render(
locale,
RegisterContext::default(), RegisterContext::default(),
query, query,
csrf_token, csrf_token,
@@ -100,6 +103,7 @@ pub(crate) async fn get(
pub(crate) async fn post( pub(crate) async fn post(
mut rng: BoxRng, mut rng: BoxRng,
clock: BoxClock, clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(password_manager): State<PasswordManager>, State(password_manager): State<PasswordManager>,
State(templates): State<Templates>, State(templates): State<Templates>,
mut policy: Policy, mut policy: Policy,
@@ -184,6 +188,7 @@ pub(crate) async fn post(
if !state.is_valid() { if !state.is_valid() {
let content = render( let content = render(
locale,
RegisterContext::default().with_form_state(state), RegisterContext::default().with_form_state(state),
query, query,
csrf_token, csrf_token,
@@ -238,6 +243,7 @@ pub(crate) async fn post(
} }
async fn render( async fn render(
locale: DataLocale,
ctx: RegisterContext, ctx: RegisterContext,
action: OptionalPostAuthAction, action: OptionalPostAuthAction,
csrf_token: CsrfToken, csrf_token: CsrfToken,
@@ -250,7 +256,7 @@ async fn render(
} else { } else {
ctx ctx
}; };
let ctx = ctx.with_csrf(csrf_token.form_value()); let ctx = ctx.with_csrf(csrf_token.form_value()).with_language(locale);
let content = templates.render_register(&ctx)?; let content = templates.render_register(&ctx)?;
Ok(content) Ok(content)

View File

@@ -45,7 +45,7 @@ impl Context {
pub fn add_missing(&self, translation_tree: &mut TranslationTree) -> usize { pub fn add_missing(&self, translation_tree: &mut TranslationTree) -> usize {
let mut count = 0; let mut count = 0;
for translatable in &self.keys { for translatable in &self.keys {
let message = Message::from_literal(translatable.default_value()); let message = Message::from_literal(String::new());
let location = translatable.location.as_ref().map(|location| { let location = translatable.location.as_ref().map(|location| {
if location.span.start_line == location.span.end_line { if location.span.start_line == location.span.end_line {
@@ -123,11 +123,4 @@ impl Key {
location: None, location: None,
} }
} }
pub fn default_value(&self) -> String {
match self.kind {
Kind::Message => self.key.clone(),
Kind::Plural => format!("%(count)d {}", self.key),
}
}
} }

View File

@@ -20,6 +20,7 @@ use mas_data_model::{
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState, AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail, UserEmailVerification, UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail, UserEmailVerification,
}; };
use mas_i18n::DataLocale;
use mas_router::{PostAuthAction, Route}; use mas_router::{PostAuthAction, Route};
use rand::Rng; use rand::Rng;
use serde::{ser::SerializeStruct, Deserialize, Serialize}; use serde::{ser::SerializeStruct, Deserialize, Serialize};
@@ -68,6 +69,17 @@ pub trait TemplateContext: Serialize {
} }
} }
/// Attach a language to the template context
fn with_language(self, lang: DataLocale) -> WithLanguage<Self>
where
Self: Sized,
{
WithLanguage {
lang: lang.to_string(),
inner: self,
}
}
/// Generate sample values for this context type /// Generate sample values for this context type
/// ///
/// This is then used to check for template validity in unit tests and in /// This is then used to check for template validity in unit tests and in
@@ -86,6 +98,30 @@ impl TemplateContext for () {
} }
} }
/// Context with a specified locale in it
#[derive(Serialize)]
pub struct WithLanguage<T> {
lang: String,
#[serde(flatten)]
inner: T,
}
impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
where
Self: Sized,
{
T::sample(now, rng)
.into_iter()
.map(|inner| WithLanguage {
lang: "en".into(),
inner,
})
.collect()
}
}
/// Context with a CSRF token in it /// Context with a CSRF token in it
#[derive(Serialize)] #[derive(Serialize)]
pub struct WithCsrf<T> { pub struct WithCsrf<T> {

View File

@@ -40,7 +40,7 @@ pub fn register(
env: &mut minijinja::Environment, env: &mut minijinja::Environment,
url_builder: UrlBuilder, url_builder: UrlBuilder,
vite_manifest: ViteManifest, vite_manifest: ViteManifest,
translator: Translator, translator: Arc<Translator>,
) { ) {
env.add_test("empty", self::tester_empty); env.add_test("empty", self::tester_empty);
env.add_test("starting_with", tester_starting_with); env.add_test("starting_with", tester_starting_with);
@@ -56,12 +56,7 @@ pub fn register(
vite_manifest, vite_manifest,
}), }),
); );
env.add_global( env.add_global("_", Value::from_object(Translate { translator }));
"_",
Value::from_object(Translate {
translations: Arc::new(translator),
}),
);
} }
fn tester_empty(seq: &dyn SeqObject) -> bool { fn tester_empty(seq: &dyn SeqObject) -> bool {
@@ -195,7 +190,7 @@ fn function_add_params_to_url(
} }
struct Translate { struct Translate {
translations: Arc<Translator>, translator: Arc<Translator>,
} }
impl std::fmt::Debug for Translate { impl std::fmt::Debug for Translate {
@@ -231,14 +226,14 @@ impl Object for Translate {
let (key, kwargs): (&str, Kwargs) = from_args(args)?; let (key, kwargs): (&str, Kwargs) = from_args(args)?;
let (message, _locale) = if let Some(count) = kwargs.get("count")? { let (message, _locale) = if let Some(count) = kwargs.get("count")? {
self.translations self.translator
.plural_with_fallback(lang, key, count) .plural_with_fallback(lang, key, count)
.ok_or(Error::new( .ok_or(Error::new(
ErrorKind::InvalidOperation, ErrorKind::InvalidOperation,
"Missing translation", "Missing translation",
))? ))?
} else { } else {
self.translations self.translator
.message_with_fallback(lang, key) .message_with_fallback(lang, key)
.ok_or(Error::new( .ok_or(Error::new(
ErrorKind::InvalidOperation, ErrorKind::InvalidOperation,

View File

@@ -29,7 +29,7 @@ use std::{collections::HashSet, sync::Arc};
use anyhow::Context as _; use anyhow::Context as _;
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use camino::{Utf8Path, Utf8PathBuf}; use camino::{Utf8Path, Utf8PathBuf};
use mas_i18n::Translator; use mas_i18n::{Translator};
use mas_router::UrlBuilder; use mas_router::UrlBuilder;
use mas_spa::ViteManifest; use mas_spa::ViteManifest;
use rand::Rng; use rand::Rng;
@@ -53,7 +53,7 @@ pub use self::{
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext, LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
PostAuthContextInner, ReauthContext, ReauthFormField, RegisterContext, RegisterFormField, PostAuthContextInner, ReauthContext, ReauthFormField, RegisterContext, RegisterFormField,
TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamSuggestLink, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamSuggestLink,
WithCsrf, WithOptionalSession, WithSession, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
}, },
forms::{FieldError, FormError, FormField, FormState, ToFormState}, forms::{FieldError, FormError, FormField, FormState, ToFormState},
}; };
@@ -71,6 +71,7 @@ pub fn escape_html(input: &str) -> String {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Templates { pub struct Templates {
environment: Arc<ArcSwap<minijinja::Environment<'static>>>, environment: Arc<ArcSwap<minijinja::Environment<'static>>>,
translator: Arc<ArcSwap<Translator>>,
url_builder: UrlBuilder, url_builder: UrlBuilder,
vite_manifest_path: Utf8PathBuf, vite_manifest_path: Utf8PathBuf,
translations_path: Utf8PathBuf, translations_path: Utf8PathBuf,
@@ -151,7 +152,7 @@ impl Templates {
vite_manifest_path: Utf8PathBuf, vite_manifest_path: Utf8PathBuf,
translations_path: Utf8PathBuf, translations_path: Utf8PathBuf,
) -> Result<Self, TemplateLoadingError> { ) -> Result<Self, TemplateLoadingError> {
let environment = Self::load_( let (translator, environment) = Self::load_(
&path, &path,
url_builder.clone(), url_builder.clone(),
&vite_manifest_path, &vite_manifest_path,
@@ -159,7 +160,8 @@ impl Templates {
) )
.await?; .await?;
Ok(Self { Ok(Self {
environment: Arc::new(ArcSwap::from_pointee(environment)), environment: Arc::new(ArcSwap::new(environment)),
translator: Arc::new(ArcSwap::new(translator)),
path, path,
url_builder, url_builder,
vite_manifest_path, vite_manifest_path,
@@ -172,7 +174,7 @@ impl Templates {
url_builder: UrlBuilder, url_builder: UrlBuilder,
vite_manifest_path: &Utf8Path, vite_manifest_path: &Utf8Path,
translations_path: &Utf8Path, translations_path: &Utf8Path,
) -> Result<minijinja::Environment<'static>, TemplateLoadingError> { ) -> Result<(Arc<Translator>, Arc<minijinja::Environment<'static>>), TemplateLoadingError> {
let path = path.to_owned(); let path = path.to_owned();
let span = tracing::Span::current(); let span = tracing::Span::current();
@@ -189,6 +191,7 @@ impl Templates {
let translator = let translator =
tokio::task::spawn_blocking(move || Translator::load_from_path(&translations_path)) tokio::task::spawn_blocking(move || Translator::load_from_path(&translations_path))
.await??; .await??;
let translator = Arc::new(translator);
let (loaded, mut env) = tokio::task::spawn_blocking(move || { let (loaded, mut env) = tokio::task::spawn_blocking(move || {
span.in_scope(move || { span.in_scope(move || {
@@ -223,14 +226,21 @@ impl Templates {
}) })
.await??; .await??;
self::functions::register(&mut env, url_builder, vite_manifest, translator); self::functions::register(
&mut env,
url_builder,
vite_manifest,
Arc::clone(&translator),
);
let env = Arc::new(env);
let needed: HashSet<_> = TEMPLATES.into_iter().map(ToOwned::to_owned).collect(); let needed: HashSet<_> = TEMPLATES.into_iter().map(ToOwned::to_owned).collect();
debug!(?loaded, ?needed, "Templates loaded"); debug!(?loaded, ?needed, "Templates loaded");
let missing: HashSet<_> = needed.difference(&loaded).cloned().collect(); let missing: HashSet<_> = needed.difference(&loaded).cloned().collect();
if missing.is_empty() { if missing.is_empty() {
Ok(env) Ok((translator, env))
} else { } else {
Err(TemplateLoadingError::MissingTemplates { missing, loaded }) Err(TemplateLoadingError::MissingTemplates { missing, loaded })
} }
@@ -244,7 +254,7 @@ impl Templates {
err, err,
)] )]
pub async fn reload(&self) -> Result<(), TemplateLoadingError> { pub async fn reload(&self) -> Result<(), TemplateLoadingError> {
let new_minijinja = Self::load_( let (translator, environment) = Self::load_(
&self.path, &self.path,
self.url_builder.clone(), self.url_builder.clone(),
&self.vite_manifest_path, &self.vite_manifest_path,
@@ -252,11 +262,18 @@ impl Templates {
) )
.await?; .await?;
// Swap it // Swap them
self.environment.store(Arc::new(new_minijinja)); self.environment.store(environment);
self.translator.store(translator);
Ok(()) Ok(())
} }
/// Get the translator
#[must_use]
pub fn translator(&self) -> Arc<Translator> {
self.translator.load_full()
}
} }
/// Failed to render a template /// Failed to render a template
@@ -287,40 +304,40 @@ pub enum TemplateError {
register_templates! { register_templates! {
/// Render the not found fallback page /// Render the not found fallback page
pub fn render_not_found(NotFoundContext) { "pages/404.html" } pub fn render_not_found(WithLanguage<NotFoundContext>) { "pages/404.html" }
/// Render the frontend app /// Render the frontend app
pub fn render_app(AppContext) { "app.html" } pub fn render_app(WithLanguage<AppContext>) { "app.html" }
/// Render the login page /// Render the login page
pub fn render_login(WithCsrf<LoginContext>) { "pages/login.html" } pub fn render_login(WithLanguage<WithCsrf<LoginContext>>) { "pages/login.html" }
/// Render the registration page /// Render the registration page
pub fn render_register(WithCsrf<RegisterContext>) { "pages/register.html" } pub fn render_register(WithLanguage<WithCsrf<RegisterContext>>) { "pages/register.html" }
/// Render the client consent page /// Render the client consent page
pub fn render_consent(WithCsrf<WithSession<ConsentContext>>) { "pages/consent.html" } pub fn render_consent(WithLanguage<WithCsrf<WithSession<ConsentContext>>>) { "pages/consent.html" }
/// Render the policy violation page /// Render the policy violation page
pub fn render_policy_violation(WithCsrf<WithSession<PolicyViolationContext>>) { "pages/policy_violation.html" } pub fn render_policy_violation(WithLanguage<WithCsrf<WithSession<PolicyViolationContext>>>) { "pages/policy_violation.html" }
/// Render the legacy SSO login consent page /// Render the legacy SSO login consent page
pub fn render_sso_login(WithCsrf<WithSession<CompatSsoContext>>) { "pages/sso.html" } pub fn render_sso_login(WithLanguage<WithCsrf<WithSession<CompatSsoContext>>>) { "pages/sso.html" }
/// Render the home page /// Render the home page
pub fn render_index(WithCsrf<WithOptionalSession<IndexContext>>) { "pages/index.html" } pub fn render_index(WithLanguage<WithCsrf<WithOptionalSession<IndexContext>>>) { "pages/index.html" }
/// Render the password change page /// Render the password change page
pub fn render_account_password(WithCsrf<WithSession<EmptyContext>>) { "pages/account/password.html" } pub fn render_account_password(WithLanguage<WithCsrf<WithSession<EmptyContext>>>) { "pages/account/password.html" }
/// Render the email verification page /// Render the email verification page
pub fn render_account_verify_email(WithCsrf<WithSession<EmailVerificationPageContext>>) { "pages/account/emails/verify.html" } pub fn render_account_verify_email(WithLanguage<WithCsrf<WithSession<EmailVerificationPageContext>>>) { "pages/account/emails/verify.html" }
/// Render the email verification page /// Render the email verification page
pub fn render_account_add_email(WithCsrf<WithSession<EmailAddContext>>) { "pages/account/emails/add.html" } pub fn render_account_add_email(WithLanguage<WithCsrf<WithSession<EmailAddContext>>>) { "pages/account/emails/add.html" }
/// Render the re-authentication form /// Render the re-authentication form
pub fn render_reauth(WithCsrf<WithSession<ReauthContext>>) { "pages/reauth.html" } pub fn render_reauth(WithLanguage<WithCsrf<WithSession<ReauthContext>>>) { "pages/reauth.html" }
/// Render the form used by the form_post response mode /// Render the form used by the form_post response mode
pub fn render_form_post<T: Serialize>(FormPostContext<T>) { "form_post.html" } pub fn render_form_post<T: Serialize>(FormPostContext<T>) { "form_post.html" }
@@ -338,13 +355,13 @@ register_templates! {
pub fn render_email_verification_subject(EmailVerificationContext) { "emails/verification.subject" } pub fn render_email_verification_subject(EmailVerificationContext) { "emails/verification.subject" }
/// Render the upstream link mismatch message /// Render the upstream link mismatch message
pub fn render_upstream_oauth2_link_mismatch(WithCsrf<WithSession<UpstreamExistingLinkContext>>) { "pages/upstream_oauth2/link_mismatch.html" } pub fn render_upstream_oauth2_link_mismatch(WithLanguage<WithCsrf<WithSession<UpstreamExistingLinkContext>>>) { "pages/upstream_oauth2/link_mismatch.html" }
/// Render the upstream suggest link message /// Render the upstream suggest link message
pub fn render_upstream_oauth2_suggest_link(WithCsrf<WithSession<UpstreamSuggestLink>>) { "pages/upstream_oauth2/suggest_link.html" } pub fn render_upstream_oauth2_suggest_link(WithLanguage<WithCsrf<WithSession<UpstreamSuggestLink>>>) { "pages/upstream_oauth2/suggest_link.html" }
/// Render the upstream register screen /// Render the upstream register screen
pub fn render_upstream_oauth2_do_register(WithCsrf<UpstreamRegister>) { "pages/upstream_oauth2/do_register.html" } pub fn render_upstream_oauth2_do_register(WithLanguage<WithCsrf<UpstreamRegister>>) { "pages/upstream_oauth2/do_register.html" }
} }
impl Templates { impl Templates {