You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-09 04:22:45 +03:00
handlers/templates: infer the language from the Accept-Language browser header
This commit is contained in:
@@ -50,6 +50,13 @@ pub struct AcceptLanguage {
|
||||
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
|
||||
const fn trim_bytes(mut bytes: &[u8]) -> &[u8] {
|
||||
// Trim leading and trailing whitespace
|
||||
|
@@ -53,6 +53,7 @@ mas-email = { path = "../email" }
|
||||
mas-graphql = { path = "../graphql" }
|
||||
mas-handlers = { path = "../handlers", default-features = false }
|
||||
mas-http = { path = "../http", default-features = false, features = ["axum", "client"] }
|
||||
mas-i18n = { path = "../i18n" }
|
||||
mas-iana = { path = "../iana" }
|
||||
mas-keystore = { path = "../keystore" }
|
||||
mas-listener = { path = "../listener" }
|
||||
|
@@ -23,6 +23,7 @@ use mas_handlers::{
|
||||
passwords::PasswordManager, ActivityTracker, BoundActivityTracker, CookieManager, ErrorWrapper,
|
||||
HttpClientFactory, MatrixHomeserver, MetadataCache, SiteConfig,
|
||||
};
|
||||
use mas_i18n::Translator;
|
||||
use mas_keystore::{Encrypter, Keystore};
|
||||
use mas_policy::{Policy, PolicyFactory};
|
||||
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 {
|
||||
fn from_ref(input: &AppState) -> Self {
|
||||
input.key_store.clone()
|
||||
|
@@ -66,6 +66,7 @@ mas-axum-utils = { path = "../axum-utils", default-features = false }
|
||||
mas-data-model = { path = "../data-model" }
|
||||
mas-graphql = { path = "../graphql" }
|
||||
mas-http = { path = "../http", default-features = false }
|
||||
mas-i18n = { path = "../i18n" }
|
||||
mas-iana = { path = "../iana" }
|
||||
mas-jose = { path = "../jose" }
|
||||
mas-keystore = { path = "../keystore" }
|
||||
|
@@ -37,6 +37,8 @@ use mas_templates::{CompatSsoContext, ErrorContext, TemplateContext, Templates};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::PreferredLanguage;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AllParams<'s> {
|
||||
#[serde(flatten)]
|
||||
@@ -58,6 +60,7 @@ pub struct Params {
|
||||
err,
|
||||
)]
|
||||
pub async fn get(
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
mut repo: BoxRepository,
|
||||
@@ -110,7 +113,8 @@ pub async fn get(
|
||||
|
||||
let ctx = CompatSsoContext::new(login)
|
||||
.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)?;
|
||||
|
||||
|
@@ -53,7 +53,7 @@ use mas_keystore::{Encrypter, Keystore};
|
||||
use mas_policy::Policy;
|
||||
use mas_router::{Route, UrlBuilder};
|
||||
use mas_storage::{BoxClock, BoxRepository, BoxRng};
|
||||
use mas_templates::{ErrorContext, NotFoundContext, Templates};
|
||||
use mas_templates::{ErrorContext, NotFoundContext, TemplateContext, Templates};
|
||||
use passwords::PasswordManager;
|
||||
use sqlx::PgPool;
|
||||
use tower::util::AndThenLayer;
|
||||
@@ -68,6 +68,7 @@ pub mod upstream_oauth2;
|
||||
mod views;
|
||||
|
||||
mod activity_tracker;
|
||||
mod preferred_language;
|
||||
mod site_config;
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
@@ -96,6 +97,7 @@ pub use self::{
|
||||
activity_tracker::{ActivityTracker, Bound as BoundActivityTracker},
|
||||
compat::MatrixHomeserver,
|
||||
graphql::schema as graphql_schema,
|
||||
preferred_language::PreferredLanguage,
|
||||
site_config::SiteConfig,
|
||||
upstream_oauth2::cache::MetadataCache,
|
||||
};
|
||||
@@ -298,6 +300,7 @@ where
|
||||
<B as HttpBody>::Error: std::error::Error + Send + Sync,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
UrlBuilder: FromRef<S>,
|
||||
PreferredLanguage: FromRequestParts<S>,
|
||||
BoxRepository: FromRequestParts<S>,
|
||||
CookieJar: FromRequestParts<S>,
|
||||
BoundActivityTracker: FromRequestParts<S>,
|
||||
@@ -433,8 +436,9 @@ pub async fn fallback(
|
||||
OriginalUri(uri): OriginalUri,
|
||||
method: Method,
|
||||
version: Version,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
) -> 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
|
||||
|
||||
let res = templates.render_not_found(&ctx)?;
|
||||
|
@@ -34,7 +34,9 @@ use tracing::warn;
|
||||
use ulid::Ulid;
|
||||
|
||||
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)]
|
||||
pub enum RouteError {
|
||||
@@ -89,6 +91,7 @@ impl_from_error_for_route!(super::callback::CallbackDestinationError);
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
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 ctx = PolicyViolationContext::new(grant, client)
|
||||
.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)?;
|
||||
|
||||
|
@@ -39,7 +39,7 @@ use thiserror::Error;
|
||||
use tracing::warn;
|
||||
|
||||
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;
|
||||
pub mod complete;
|
||||
@@ -139,6 +139,7 @@ fn resolve_response_mode(
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(key_store): State<Keystore>,
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
@@ -418,7 +419,8 @@ pub(crate) async fn get(
|
||||
|
||||
let ctx = PolicyViolationContext::new(grant, client)
|
||||
.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)?;
|
||||
Html(content).into_response()
|
||||
|
@@ -34,7 +34,7 @@ use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Tem
|
||||
use thiserror::Error;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{impl_from_error_for_route, BoundActivityTracker};
|
||||
use crate::{impl_from_error_for_route, BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RouteError {
|
||||
@@ -82,6 +82,7 @@ impl IntoResponse for RouteError {
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
mut policy: Policy,
|
||||
mut repo: BoxRepository,
|
||||
@@ -123,7 +124,8 @@ pub(crate) async fn get(
|
||||
if res.valid() {
|
||||
let ctx = ConsentContext::new(grant, client)
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value());
|
||||
.with_csrf(csrf_token.form_value())
|
||||
.with_language(locale);
|
||||
|
||||
let content = templates.render_consent(&ctx)?;
|
||||
|
||||
@@ -131,7 +133,8 @@ pub(crate) async fn get(
|
||||
} else {
|
||||
let ctx = PolicyViolationContext::new(grant, client)
|
||||
.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)?;
|
||||
|
||||
|
53
crates/handlers/src/preferred_language.rs
Normal file
53
crates/handlers/src/preferred_language.rs
Normal 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))
|
||||
}
|
||||
}
|
@@ -33,6 +33,7 @@ use hyper::{
|
||||
use mas_axum_utils::{
|
||||
cookies::CookieManager, http_client_factory::HttpClientFactory, ErrorWrapper,
|
||||
};
|
||||
use mas_i18n::Translator;
|
||||
use mas_keystore::{Encrypter, JsonWebKey, JsonWebKeySet, Keystore, PrivateKey};
|
||||
use mas_matrix::{HomeserverConnection, MockHomeserverConnection};
|
||||
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 {
|
||||
fn from_ref(input: &TestState) -> Self {
|
||||
input.key_store.clone()
|
||||
|
@@ -42,7 +42,7 @@ use thiserror::Error;
|
||||
use ulid::Ulid;
|
||||
|
||||
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)]
|
||||
pub(crate) enum RouteError {
|
||||
@@ -189,6 +189,7 @@ pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
mut repo: BoxRepository,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
cookie_jar: CookieJar,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
@@ -264,7 +265,8 @@ pub(crate) async fn get(
|
||||
|
||||
let ctx = UpstreamExistingLinkContext::new(user)
|
||||
.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()
|
||||
}
|
||||
@@ -273,7 +275,8 @@ pub(crate) async fn get(
|
||||
// Session not linked, but user logged in: suggest linking account
|
||||
let ctx = UpstreamSuggestLink::new(&link)
|
||||
.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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ use mas_storage::{
|
||||
use mas_templates::{EmailAddContext, ErrorContext, TemplateContext, Templates};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker};
|
||||
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct EmailForm {
|
||||
@@ -42,6 +42,7 @@ pub struct EmailForm {
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
mut repo: BoxRepository,
|
||||
@@ -63,7 +64,8 @@ pub(crate) async fn get(
|
||||
|
||||
let ctx = EmailAddContext::new()
|
||||
.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)?;
|
||||
|
||||
|
@@ -32,7 +32,7 @@ use mas_templates::{EmailVerificationPageContext, TemplateContext, Templates};
|
||||
use serde::Deserialize;
|
||||
use ulid::Ulid;
|
||||
|
||||
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker};
|
||||
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CodeForm {
|
||||
@@ -48,6 +48,7 @@ pub struct CodeForm {
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
mut repo: BoxRepository,
|
||||
@@ -84,7 +85,8 @@ pub(crate) async fn get(
|
||||
|
||||
let ctx = EmailVerificationPageContext::new(user_email)
|
||||
.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)?;
|
||||
|
||||
|
@@ -24,6 +24,7 @@ use mas_axum_utils::{
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_data_model::BrowserSession;
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_policy::Policy;
|
||||
use mas_router::Route;
|
||||
use mas_storage::{
|
||||
@@ -35,7 +36,7 @@ use rand::Rng;
|
||||
use serde::Deserialize;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::{passwords::PasswordManager, BoundActivityTracker};
|
||||
use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChangeForm {
|
||||
@@ -48,6 +49,7 @@ pub struct ChangeForm {
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(password_manager): State<PasswordManager>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
@@ -68,7 +70,7 @@ pub(crate) async fn get(
|
||||
.record_browser_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
render(&mut rng, &clock, templates, session, cookie_jar).await
|
||||
render(&mut rng, &clock, locale, templates, session, cookie_jar).await
|
||||
} else {
|
||||
let login = mas_router::Login::and_then(mas_router::PostAuthAction::ChangePassword);
|
||||
Ok((cookie_jar, login.go()).into_response())
|
||||
@@ -78,6 +80,7 @@ pub(crate) async fn get(
|
||||
async fn render(
|
||||
rng: impl Rng + Send,
|
||||
clock: &impl Clock,
|
||||
locale: DataLocale,
|
||||
templates: Templates,
|
||||
session: BrowserSession,
|
||||
cookie_jar: CookieJar,
|
||||
@@ -86,7 +89,8 @@ async fn render(
|
||||
|
||||
let ctx = EmptyContext
|
||||
.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)?;
|
||||
|
||||
@@ -97,6 +101,7 @@ async fn render(
|
||||
pub(crate) async fn post(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(password_manager): State<PasswordManager>,
|
||||
State(templates): State<Templates>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
@@ -172,7 +177,15 @@ pub(crate) async fn post(
|
||||
.record_browser_session(&clock, &session)
|
||||
.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?;
|
||||
|
||||
|
@@ -19,12 +19,13 @@ use axum::{
|
||||
use mas_axum_utils::{cookies::CookieJar, FancyError, SessionInfoExt};
|
||||
use mas_router::{PostAuthAction, Route};
|
||||
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)]
|
||||
pub async fn get(
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
action: Option<Query<mas_router::AccountAction>>,
|
||||
@@ -49,7 +50,7 @@ pub async fn get(
|
||||
.record_browser_session(&clock, &session)
|
||||
.await;
|
||||
|
||||
let ctx = AppContext::default();
|
||||
let ctx = AppContext::default().with_language(locale);
|
||||
let content = templates.render_app(&ctx)?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
|
@@ -21,7 +21,7 @@ use mas_router::UrlBuilder;
|
||||
use mas_storage::{BoxClock, BoxRepository, BoxRng};
|
||||
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)]
|
||||
pub async fn get(
|
||||
@@ -32,7 +32,9 @@ pub async fn get(
|
||||
State(url_builder): State<UrlBuilder>,
|
||||
mut repo: BoxRepository,
|
||||
cookie_jar: CookieJar,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
) -> Result<impl IntoResponse, FancyError> {
|
||||
tracing::info!("{locale}");
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
|
||||
let (session_info, cookie_jar) = cookie_jar.session_info();
|
||||
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())
|
||||
.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)?;
|
||||
|
||||
|
@@ -25,6 +25,7 @@ use mas_axum_utils::{
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_data_model::BrowserSession;
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_router::{Route, UpstreamOAuth2Authorize};
|
||||
use mas_storage::{
|
||||
upstream_oauth2::UpstreamOAuthProviderRepository,
|
||||
@@ -39,7 +40,7 @@ use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::shared::OptionalPostAuthAction;
|
||||
use crate::{passwords::PasswordManager, BoundActivityTracker};
|
||||
use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub(crate) struct LoginForm {
|
||||
@@ -55,6 +56,7 @@ impl ToFormState for LoginForm {
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(password_manager): State<PasswordManager>,
|
||||
State(templates): State<Templates>,
|
||||
mut repo: BoxRepository,
|
||||
@@ -93,6 +95,7 @@ pub(crate) async fn get(
|
||||
};
|
||||
|
||||
let content = render(
|
||||
locale,
|
||||
LoginContext::default()
|
||||
// XXX: we might want to have a site-wide config in the templates context instead?
|
||||
.with_password_login(password_manager.is_enabled())
|
||||
@@ -111,6 +114,7 @@ pub(crate) async fn get(
|
||||
pub(crate) async fn post(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(password_manager): State<PasswordManager>,
|
||||
State(templates): State<Templates>,
|
||||
mut repo: BoxRepository,
|
||||
@@ -148,6 +152,7 @@ pub(crate) async fn post(
|
||||
if !state.is_valid() {
|
||||
let providers = repo.upstream_oauth_provider().all().await?;
|
||||
let content = render(
|
||||
locale,
|
||||
LoginContext::default()
|
||||
.with_form_state(state)
|
||||
.with_upstream_providers(providers),
|
||||
@@ -187,6 +192,7 @@ pub(crate) async fn post(
|
||||
let state = state.with_error_on_form(e);
|
||||
|
||||
let content = render(
|
||||
locale,
|
||||
LoginContext::default().with_form_state(state),
|
||||
query,
|
||||
csrf_token,
|
||||
@@ -275,6 +281,7 @@ async fn login(
|
||||
}
|
||||
|
||||
async fn render(
|
||||
locale: DataLocale,
|
||||
ctx: LoginContext,
|
||||
action: OptionalPostAuthAction,
|
||||
csrf_token: CsrfToken,
|
||||
@@ -287,7 +294,7 @@ async fn render(
|
||||
} else {
|
||||
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)?;
|
||||
Ok(content)
|
||||
|
@@ -33,7 +33,7 @@ use serde::Deserialize;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::shared::OptionalPostAuthAction;
|
||||
use crate::{passwords::PasswordManager, BoundActivityTracker};
|
||||
use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct ReauthForm {
|
||||
@@ -44,6 +44,7 @@ pub(crate) struct ReauthForm {
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(password_manager): State<PasswordManager>,
|
||||
State(templates): State<Templates>,
|
||||
activity_tracker: BoundActivityTracker,
|
||||
@@ -79,7 +80,10 @@ pub(crate) async fn get(
|
||||
} else {
|
||||
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)?;
|
||||
|
||||
|
@@ -27,6 +27,7 @@ use mas_axum_utils::{
|
||||
csrf::{CsrfExt, CsrfToken, ProtectedForm},
|
||||
FancyError, SessionInfoExt,
|
||||
};
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_policy::Policy;
|
||||
use mas_router::Route;
|
||||
use mas_storage::{
|
||||
@@ -42,7 +43,7 @@ use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use super::shared::OptionalPostAuthAction;
|
||||
use crate::{passwords::PasswordManager, BoundActivityTracker};
|
||||
use crate::{passwords::PasswordManager, BoundActivityTracker, PreferredLanguage};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub(crate) struct RegisterForm {
|
||||
@@ -60,6 +61,7 @@ impl ToFormState for RegisterForm {
|
||||
pub(crate) async fn get(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(templates): State<Templates>,
|
||||
State(password_manager): State<PasswordManager>,
|
||||
mut repo: BoxRepository,
|
||||
@@ -84,6 +86,7 @@ pub(crate) async fn get(
|
||||
}
|
||||
|
||||
let content = render(
|
||||
locale,
|
||||
RegisterContext::default(),
|
||||
query,
|
||||
csrf_token,
|
||||
@@ -100,6 +103,7 @@ pub(crate) async fn get(
|
||||
pub(crate) async fn post(
|
||||
mut rng: BoxRng,
|
||||
clock: BoxClock,
|
||||
PreferredLanguage(locale): PreferredLanguage,
|
||||
State(password_manager): State<PasswordManager>,
|
||||
State(templates): State<Templates>,
|
||||
mut policy: Policy,
|
||||
@@ -184,6 +188,7 @@ pub(crate) async fn post(
|
||||
|
||||
if !state.is_valid() {
|
||||
let content = render(
|
||||
locale,
|
||||
RegisterContext::default().with_form_state(state),
|
||||
query,
|
||||
csrf_token,
|
||||
@@ -238,6 +243,7 @@ pub(crate) async fn post(
|
||||
}
|
||||
|
||||
async fn render(
|
||||
locale: DataLocale,
|
||||
ctx: RegisterContext,
|
||||
action: OptionalPostAuthAction,
|
||||
csrf_token: CsrfToken,
|
||||
@@ -250,7 +256,7 @@ async fn render(
|
||||
} else {
|
||||
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)?;
|
||||
Ok(content)
|
||||
|
@@ -45,7 +45,7 @@ impl Context {
|
||||
pub fn add_missing(&self, translation_tree: &mut TranslationTree) -> usize {
|
||||
let mut count = 0;
|
||||
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| {
|
||||
if location.span.start_line == location.span.end_line {
|
||||
@@ -123,11 +123,4 @@ impl Key {
|
||||
location: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_value(&self) -> String {
|
||||
match self.kind {
|
||||
Kind::Message => self.key.clone(),
|
||||
Kind::Plural => format!("%(count)d {}", self.key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ use mas_data_model::{
|
||||
AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
|
||||
UpstreamOAuthLink, UpstreamOAuthProvider, User, UserEmail, UserEmailVerification,
|
||||
};
|
||||
use mas_i18n::DataLocale;
|
||||
use mas_router::{PostAuthAction, Route};
|
||||
use rand::Rng;
|
||||
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
|
||||
///
|
||||
/// 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
|
||||
#[derive(Serialize)]
|
||||
pub struct WithCsrf<T> {
|
||||
|
@@ -40,7 +40,7 @@ pub fn register(
|
||||
env: &mut minijinja::Environment,
|
||||
url_builder: UrlBuilder,
|
||||
vite_manifest: ViteManifest,
|
||||
translator: Translator,
|
||||
translator: Arc<Translator>,
|
||||
) {
|
||||
env.add_test("empty", self::tester_empty);
|
||||
env.add_test("starting_with", tester_starting_with);
|
||||
@@ -56,12 +56,7 @@ pub fn register(
|
||||
vite_manifest,
|
||||
}),
|
||||
);
|
||||
env.add_global(
|
||||
"_",
|
||||
Value::from_object(Translate {
|
||||
translations: Arc::new(translator),
|
||||
}),
|
||||
);
|
||||
env.add_global("_", Value::from_object(Translate { translator }));
|
||||
}
|
||||
|
||||
fn tester_empty(seq: &dyn SeqObject) -> bool {
|
||||
@@ -195,7 +190,7 @@ fn function_add_params_to_url(
|
||||
}
|
||||
|
||||
struct Translate {
|
||||
translations: Arc<Translator>,
|
||||
translator: Arc<Translator>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Translate {
|
||||
@@ -231,14 +226,14 @@ impl Object for Translate {
|
||||
let (key, kwargs): (&str, Kwargs) = from_args(args)?;
|
||||
|
||||
let (message, _locale) = if let Some(count) = kwargs.get("count")? {
|
||||
self.translations
|
||||
self.translator
|
||||
.plural_with_fallback(lang, key, count)
|
||||
.ok_or(Error::new(
|
||||
ErrorKind::InvalidOperation,
|
||||
"Missing translation",
|
||||
))?
|
||||
} else {
|
||||
self.translations
|
||||
self.translator
|
||||
.message_with_fallback(lang, key)
|
||||
.ok_or(Error::new(
|
||||
ErrorKind::InvalidOperation,
|
||||
|
@@ -29,7 +29,7 @@ use std::{collections::HashSet, sync::Arc};
|
||||
use anyhow::Context as _;
|
||||
use arc_swap::ArcSwap;
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use mas_i18n::Translator;
|
||||
use mas_i18n::{Translator};
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_spa::ViteManifest;
|
||||
use rand::Rng;
|
||||
@@ -53,7 +53,7 @@ pub use self::{
|
||||
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
|
||||
PostAuthContextInner, ReauthContext, ReauthFormField, RegisterContext, RegisterFormField,
|
||||
TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamSuggestLink,
|
||||
WithCsrf, WithOptionalSession, WithSession,
|
||||
WithCsrf, WithLanguage, WithOptionalSession, WithSession,
|
||||
},
|
||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||
};
|
||||
@@ -71,6 +71,7 @@ pub fn escape_html(input: &str) -> String {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Templates {
|
||||
environment: Arc<ArcSwap<minijinja::Environment<'static>>>,
|
||||
translator: Arc<ArcSwap<Translator>>,
|
||||
url_builder: UrlBuilder,
|
||||
vite_manifest_path: Utf8PathBuf,
|
||||
translations_path: Utf8PathBuf,
|
||||
@@ -151,7 +152,7 @@ impl Templates {
|
||||
vite_manifest_path: Utf8PathBuf,
|
||||
translations_path: Utf8PathBuf,
|
||||
) -> Result<Self, TemplateLoadingError> {
|
||||
let environment = Self::load_(
|
||||
let (translator, environment) = Self::load_(
|
||||
&path,
|
||||
url_builder.clone(),
|
||||
&vite_manifest_path,
|
||||
@@ -159,7 +160,8 @@ impl Templates {
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
environment: Arc::new(ArcSwap::from_pointee(environment)),
|
||||
environment: Arc::new(ArcSwap::new(environment)),
|
||||
translator: Arc::new(ArcSwap::new(translator)),
|
||||
path,
|
||||
url_builder,
|
||||
vite_manifest_path,
|
||||
@@ -172,7 +174,7 @@ impl Templates {
|
||||
url_builder: UrlBuilder,
|
||||
vite_manifest_path: &Utf8Path,
|
||||
translations_path: &Utf8Path,
|
||||
) -> Result<minijinja::Environment<'static>, TemplateLoadingError> {
|
||||
) -> Result<(Arc<Translator>, Arc<minijinja::Environment<'static>>), TemplateLoadingError> {
|
||||
let path = path.to_owned();
|
||||
let span = tracing::Span::current();
|
||||
|
||||
@@ -189,6 +191,7 @@ impl Templates {
|
||||
let translator =
|
||||
tokio::task::spawn_blocking(move || Translator::load_from_path(&translations_path))
|
||||
.await??;
|
||||
let translator = Arc::new(translator);
|
||||
|
||||
let (loaded, mut env) = tokio::task::spawn_blocking(move || {
|
||||
span.in_scope(move || {
|
||||
@@ -223,14 +226,21 @@ impl Templates {
|
||||
})
|
||||
.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();
|
||||
debug!(?loaded, ?needed, "Templates loaded");
|
||||
let missing: HashSet<_> = needed.difference(&loaded).cloned().collect();
|
||||
|
||||
if missing.is_empty() {
|
||||
Ok(env)
|
||||
Ok((translator, env))
|
||||
} else {
|
||||
Err(TemplateLoadingError::MissingTemplates { missing, loaded })
|
||||
}
|
||||
@@ -244,7 +254,7 @@ impl Templates {
|
||||
err,
|
||||
)]
|
||||
pub async fn reload(&self) -> Result<(), TemplateLoadingError> {
|
||||
let new_minijinja = Self::load_(
|
||||
let (translator, environment) = Self::load_(
|
||||
&self.path,
|
||||
self.url_builder.clone(),
|
||||
&self.vite_manifest_path,
|
||||
@@ -252,11 +262,18 @@ impl Templates {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Swap it
|
||||
self.environment.store(Arc::new(new_minijinja));
|
||||
// Swap them
|
||||
self.environment.store(environment);
|
||||
self.translator.store(translator);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the translator
|
||||
#[must_use]
|
||||
pub fn translator(&self) -> Arc<Translator> {
|
||||
self.translator.load_full()
|
||||
}
|
||||
}
|
||||
|
||||
/// Failed to render a template
|
||||
@@ -287,40 +304,40 @@ pub enum TemplateError {
|
||||
|
||||
register_templates! {
|
||||
/// 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
|
||||
pub fn render_app(AppContext) { "app.html" }
|
||||
pub fn render_app(WithLanguage<AppContext>) { "app.html" }
|
||||
|
||||
/// 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
|
||||
pub fn render_register(WithCsrf<RegisterContext>) { "pages/register.html" }
|
||||
pub fn render_register(WithLanguage<WithCsrf<RegisterContext>>) { "pages/register.html" }
|
||||
|
||||
/// 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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" }
|
||||
|
||||
/// 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
|
||||
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
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user