You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-07 17:03:01 +03:00
handlers/templates: infer the language from the Accept-Language browser header
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -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",
|
||||||
|
@@ -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
|
||||||
|
@@ -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" }
|
||||||
|
@@ -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()
|
||||||
|
@@ -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" }
|
||||||
|
@@ -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)?;
|
||||||
|
|
||||||
|
@@ -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)?;
|
||||||
|
@@ -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)?;
|
||||||
|
|
||||||
|
@@ -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()
|
||||||
|
@@ -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)?;
|
||||||
|
|
||||||
|
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::{
|
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()
|
||||||
|
@@ -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()
|
||||||
}
|
}
|
||||||
|
@@ -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)?;
|
||||||
|
|
||||||
|
@@ -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)?;
|
||||||
|
|
||||||
|
@@ -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?;
|
||||||
|
|
||||||
|
@@ -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())
|
||||||
|
@@ -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)?;
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)?;
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -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> {
|
||||||
|
@@ -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,
|
||||||
|
@@ -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 {
|
||||||
|
Reference in New Issue
Block a user