You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Add instance privacy policy, TOS and imprint, and loads of design cleanups
This commit is contained in:
@ -30,9 +30,7 @@ struct AcceptLanguagePart {
|
||||
|
||||
impl PartialOrd for AcceptLanguagePart {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
// When comparing two AcceptLanguage structs, we only consider the
|
||||
// quality, in reverse.
|
||||
Reverse(self.quality).partial_cmp(&Reverse(other.quality))
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,13 @@ impl Options {
|
||||
);
|
||||
|
||||
// Load and compile the templates
|
||||
let templates = templates_from_config(&config.templates, &url_builder).await?;
|
||||
let templates = templates_from_config(
|
||||
&config.templates,
|
||||
&config.branding,
|
||||
&url_builder,
|
||||
&config.matrix.homeserver,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let http_client_factory = HttpClientFactory::new().await?;
|
||||
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
|
||||
use clap::Parser;
|
||||
use mas_config::TemplatesConfig;
|
||||
use mas_config::{BrandingConfig, MatrixConfig, TemplatesConfig};
|
||||
use mas_storage::{Clock, SystemClock};
|
||||
use rand::SeedableRng;
|
||||
use tracing::info_span;
|
||||
@ -39,13 +39,22 @@ impl Options {
|
||||
SC::Check => {
|
||||
let _span = info_span!("cli.templates.check").entered();
|
||||
|
||||
let config: TemplatesConfig = root.load_config()?;
|
||||
let template_config: TemplatesConfig = root.load_config()?;
|
||||
let branding_config: BrandingConfig = root.load_config()?;
|
||||
let matrix_config: MatrixConfig = root.load_config()?;
|
||||
|
||||
let clock = SystemClock::default();
|
||||
// XXX: we should disallow SeedableRng::from_entropy
|
||||
let mut rng = rand_chacha::ChaChaRng::from_entropy();
|
||||
let url_builder =
|
||||
mas_router::UrlBuilder::new("https://example.com/".parse()?, None, None);
|
||||
let templates = templates_from_config(&config, &url_builder).await?;
|
||||
let templates = templates_from_config(
|
||||
&template_config,
|
||||
&branding_config,
|
||||
&url_builder,
|
||||
&matrix_config.homeserver,
|
||||
)
|
||||
.await?;
|
||||
templates.check_render(clock.now(), &mut rng)?;
|
||||
|
||||
Ok(())
|
||||
|
@ -44,7 +44,13 @@ impl Options {
|
||||
);
|
||||
|
||||
// Load and compile the templates
|
||||
let templates = templates_from_config(&config.templates, &url_builder).await?;
|
||||
let templates = templates_from_config(
|
||||
&config.templates,
|
||||
&config.branding,
|
||||
&url_builder,
|
||||
&config.matrix.homeserver,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mailer = mailer_from_config(&config.email, &templates)?;
|
||||
mailer.test_connection().await?;
|
||||
|
@ -16,14 +16,14 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use mas_config::{
|
||||
DatabaseConfig, DatabaseConnectConfig, EmailConfig, EmailSmtpMode, EmailTransportConfig,
|
||||
PasswordsConfig, PolicyConfig, TemplatesConfig,
|
||||
BrandingConfig, DatabaseConfig, DatabaseConnectConfig, EmailConfig, EmailSmtpMode,
|
||||
EmailTransportConfig, PasswordsConfig, PolicyConfig, TemplatesConfig,
|
||||
};
|
||||
use mas_email::{MailTransport, Mailer};
|
||||
use mas_handlers::{passwords::PasswordManager, ActivityTracker};
|
||||
use mas_policy::PolicyFactory;
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_templates::{TemplateLoadingError, Templates};
|
||||
use mas_templates::{SiteBranding, TemplateLoadingError, Templates};
|
||||
use sqlx::{
|
||||
postgres::{PgConnectOptions, PgPoolOptions},
|
||||
ConnectOptions, PgConnection, PgPool,
|
||||
@ -116,13 +116,34 @@ pub async fn policy_factory_from_config(
|
||||
|
||||
pub async fn templates_from_config(
|
||||
config: &TemplatesConfig,
|
||||
branding: &BrandingConfig,
|
||||
url_builder: &UrlBuilder,
|
||||
server_name: &str,
|
||||
) -> Result<Templates, TemplateLoadingError> {
|
||||
let mut site_branding = SiteBranding::new(server_name);
|
||||
|
||||
if let Some(service_name) = branding.service_name.as_deref() {
|
||||
site_branding = site_branding.with_service_name(service_name);
|
||||
}
|
||||
|
||||
if let Some(policy_uri) = &branding.policy_uri {
|
||||
site_branding = site_branding.with_policy_uri(policy_uri.as_str());
|
||||
}
|
||||
|
||||
if let Some(tos_uri) = &branding.tos_uri {
|
||||
site_branding = site_branding.with_tos_uri(tos_uri.as_str());
|
||||
}
|
||||
|
||||
if let Some(imprint) = branding.imprint.as_deref() {
|
||||
site_branding = site_branding.with_imprint(imprint);
|
||||
}
|
||||
|
||||
Templates::load(
|
||||
config.path.clone(),
|
||||
url_builder.clone(),
|
||||
config.assets_manifest.clone(),
|
||||
config.translations_path.clone(),
|
||||
site_branding,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
63
crates/config/src/sections/branding.rs
Normal file
63
crates/config/src/sections/branding.rs
Normal file
@ -0,0 +1,63 @@
|
||||
// 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 async_trait::async_trait;
|
||||
use rand::Rng;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::ConfigurationSection;
|
||||
|
||||
/// Configuration section for tweaking the branding of the service
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize, Default)]
|
||||
pub struct BrandingConfig {
|
||||
/// A human-readable name. Defaults to the server's address.
|
||||
pub service_name: Option<String>,
|
||||
|
||||
/// Link to a privacy policy, displayed in the footer of web pages and
|
||||
/// emails. It is also advertised to clients through the `op_policy_uri`
|
||||
/// OIDC provider metadata.
|
||||
pub policy_uri: Option<Url>,
|
||||
|
||||
/// Link to a terms of service document, displayed in the footer of web
|
||||
/// pages and emails. It is also advertised to clients through the
|
||||
/// `op_tos_uri` OIDC provider metadata.
|
||||
pub tos_uri: Option<Url>,
|
||||
|
||||
/// Legal imprint, displayed in the footer in the footer of web pages and
|
||||
/// emails.
|
||||
pub imprint: Option<String>,
|
||||
|
||||
/// Logo displayed in some web pages.
|
||||
pub logo_uri: Option<Url>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ConfigurationSection for BrandingConfig {
|
||||
fn path() -> &'static str {
|
||||
"branding"
|
||||
}
|
||||
|
||||
async fn generate<R>(_rng: R) -> anyhow::Result<Self>
|
||||
where
|
||||
R: Rng + Send,
|
||||
{
|
||||
Ok(Self::default())
|
||||
}
|
||||
|
||||
fn test() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ use rand::Rng;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod branding;
|
||||
mod clients;
|
||||
mod database;
|
||||
mod email;
|
||||
@ -31,6 +32,7 @@ mod templates;
|
||||
mod upstream_oauth2;
|
||||
|
||||
pub use self::{
|
||||
branding::BrandingConfig,
|
||||
clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
|
||||
database::{ConnectConfig as DatabaseConnectConfig, DatabaseConfig},
|
||||
email::{EmailConfig, EmailSmtpMode, EmailTransportConfig},
|
||||
@ -103,6 +105,10 @@ pub struct RootConfig {
|
||||
#[serde(default)]
|
||||
pub upstream_oauth2: UpstreamOAuth2Config,
|
||||
|
||||
/// Configuration section for tweaking the branding of the service
|
||||
#[serde(default)]
|
||||
pub branding: BrandingConfig,
|
||||
|
||||
/// Experimental configuration options
|
||||
#[serde(default)]
|
||||
pub experimental: ExperimentalConfig,
|
||||
@ -130,6 +136,7 @@ impl ConfigurationSection for RootConfig {
|
||||
matrix: MatrixConfig::generate(&mut rng).await?,
|
||||
policy: PolicyConfig::generate(&mut rng).await?,
|
||||
upstream_oauth2: UpstreamOAuth2Config::generate(&mut rng).await?,
|
||||
branding: BrandingConfig::generate(&mut rng).await?,
|
||||
experimental: ExperimentalConfig::generate(&mut rng).await?,
|
||||
})
|
||||
}
|
||||
@ -147,6 +154,7 @@ impl ConfigurationSection for RootConfig {
|
||||
matrix: MatrixConfig::test(),
|
||||
policy: PolicyConfig::test(),
|
||||
upstream_oauth2: UpstreamOAuth2Config::test(),
|
||||
branding: BrandingConfig::test(),
|
||||
experimental: ExperimentalConfig::test(),
|
||||
}
|
||||
}
|
||||
@ -178,6 +186,9 @@ pub struct AppConfig {
|
||||
#[serde(default)]
|
||||
pub policy: PolicyConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub branding: BrandingConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub experimental: ExperimentalConfig,
|
||||
}
|
||||
@ -201,6 +212,7 @@ impl ConfigurationSection for AppConfig {
|
||||
secrets: SecretsConfig::generate(&mut rng).await?,
|
||||
matrix: MatrixConfig::generate(&mut rng).await?,
|
||||
policy: PolicyConfig::generate(&mut rng).await?,
|
||||
branding: BrandingConfig::generate(&mut rng).await?,
|
||||
experimental: ExperimentalConfig::generate(&mut rng).await?,
|
||||
})
|
||||
}
|
||||
@ -215,6 +227,7 @@ impl ConfigurationSection for AppConfig {
|
||||
secrets: SecretsConfig::test(),
|
||||
matrix: MatrixConfig::test(),
|
||||
policy: PolicyConfig::test(),
|
||||
branding: BrandingConfig::test(),
|
||||
experimental: ExperimentalConfig::test(),
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ use mas_policy::{InstantiateError, Policy, PolicyFactory};
|
||||
use mas_router::{SimpleRoute, UrlBuilder};
|
||||
use mas_storage::{clock::MockClock, BoxClock, BoxRepository, BoxRng, Repository};
|
||||
use mas_storage_pg::{DatabaseError, PgRepository};
|
||||
use mas_templates::Templates;
|
||||
use mas_templates::{SiteBranding, Templates};
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaChaRng;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
@ -116,11 +116,14 @@ impl TestState {
|
||||
|
||||
let url_builder = UrlBuilder::new("https://example.com/".parse()?, None, None);
|
||||
|
||||
let site_branding = SiteBranding::new("example.com").with_service_name("Example");
|
||||
|
||||
let templates = Templates::load(
|
||||
workspace_root.join("templates"),
|
||||
url_builder.clone(),
|
||||
workspace_root.join("frontend/dist/manifest.json"),
|
||||
workspace_root.join("translations"),
|
||||
site_branding,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -54,8 +54,8 @@ impl SynapseConnection {
|
||||
.uri(
|
||||
self.endpoint
|
||||
.join(url)
|
||||
.map(Url::into)
|
||||
.unwrap_or(String::new()),
|
||||
.map(String::from)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.header(AUTHORIZATION, format!("Bearer {}", self.access_token))
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use serde::Serialize;
|
||||
use url::Url;
|
||||
@ -41,7 +41,7 @@ pub trait Route {
|
||||
fn absolute_url(&self, base: &Url) -> Url {
|
||||
let relative = self.path_and_query();
|
||||
let relative = relative.trim_start_matches('/');
|
||||
base.join(relative.borrow()).unwrap()
|
||||
base.join(relative).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,8 @@
|
||||
|
||||
//! Contexts used in templates
|
||||
|
||||
mod branding;
|
||||
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
@ -29,6 +31,7 @@ use serde::{ser::SerializeStruct, Deserialize, Serialize};
|
||||
use ulid::Ulid;
|
||||
use url::Url;
|
||||
|
||||
pub use self::branding::SiteBranding;
|
||||
use crate::{FormField, FormState};
|
||||
|
||||
/// Helper trait to construct context wrappers
|
||||
|
89
crates/templates/src/context/branding.rs
Normal file
89
crates/templates/src/context/branding.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use minijinja::{value::StructObject, Value};
|
||||
|
||||
/// Site branding information.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SiteBranding {
|
||||
server_name: Arc<str>,
|
||||
service_name: Option<Arc<str>>,
|
||||
policy_uri: Option<Arc<str>>,
|
||||
tos_uri: Option<Arc<str>>,
|
||||
imprint: Option<Arc<str>>,
|
||||
logo_uri: Option<Arc<str>>,
|
||||
}
|
||||
|
||||
impl SiteBranding {
|
||||
/// Create a new site branding based on the given server name.
|
||||
#[must_use]
|
||||
pub fn new(server_name: impl Into<Arc<str>>) -> Self {
|
||||
Self {
|
||||
server_name: server_name.into(),
|
||||
service_name: None,
|
||||
policy_uri: None,
|
||||
tos_uri: None,
|
||||
imprint: None,
|
||||
logo_uri: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the service name.
|
||||
#[must_use]
|
||||
pub fn with_service_name(mut self, service_name: impl Into<Arc<str>>) -> Self {
|
||||
self.service_name = Some(service_name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the policy URI.
|
||||
#[must_use]
|
||||
pub fn with_policy_uri(mut self, policy_uri: impl Into<Arc<str>>) -> Self {
|
||||
self.policy_uri = Some(policy_uri.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the terms of service URI.
|
||||
#[must_use]
|
||||
pub fn with_tos_uri(mut self, tos_uri: impl Into<Arc<str>>) -> Self {
|
||||
self.tos_uri = Some(tos_uri.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the imprint.
|
||||
#[must_use]
|
||||
pub fn with_imprint(mut self, imprint: impl Into<Arc<str>>) -> Self {
|
||||
self.imprint = Some(imprint.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the logo URI.
|
||||
#[must_use]
|
||||
pub fn with_logo_uri(mut self, logo_uri: impl Into<Arc<str>>) -> Self {
|
||||
self.logo_uri = Some(logo_uri.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl StructObject for SiteBranding {
|
||||
fn get_field(&self, name: &str) -> Option<Value> {
|
||||
match name {
|
||||
"server_name" => Some(self.server_name.clone().into()),
|
||||
"service_name" => self.service_name.clone().map(Value::from),
|
||||
"policy_uri" => self.policy_uri.clone().map(Value::from),
|
||||
"tos_uri" => self.tos_uri.clone().map(Value::from),
|
||||
"imprint" => self.imprint.clone().map(Value::from),
|
||||
"logo_uri" => self.logo_uri.clone().map(Value::from),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn static_fields(&self) -> Option<&'static [&'static str]> {
|
||||
Some(&[
|
||||
"server_name",
|
||||
"service_name",
|
||||
"policy_uri",
|
||||
"tos_uri",
|
||||
"imprint",
|
||||
"logo_uri",
|
||||
])
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ use camino::{Utf8Path, Utf8PathBuf};
|
||||
use mas_i18n::Translator;
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_spa::ViteManifest;
|
||||
use minijinja::Value;
|
||||
use rand::Rng;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
@ -52,8 +53,8 @@ pub use self::{
|
||||
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
|
||||
LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext,
|
||||
PostAuthContextInner, ReauthContext, ReauthFormField, RegisterContext, RegisterFormField,
|
||||
TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, UpstreamSuggestLink,
|
||||
WithCsrf, WithLanguage, WithOptionalSession, WithSession,
|
||||
SiteBranding, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister,
|
||||
UpstreamSuggestLink, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
|
||||
},
|
||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||
};
|
||||
@ -73,6 +74,7 @@ pub struct Templates {
|
||||
environment: Arc<ArcSwap<minijinja::Environment<'static>>>,
|
||||
translator: Arc<ArcSwap<Translator>>,
|
||||
url_builder: UrlBuilder,
|
||||
branding: SiteBranding,
|
||||
vite_manifest_path: Utf8PathBuf,
|
||||
translations_path: Utf8PathBuf,
|
||||
path: Utf8PathBuf,
|
||||
@ -151,12 +153,14 @@ impl Templates {
|
||||
url_builder: UrlBuilder,
|
||||
vite_manifest_path: Utf8PathBuf,
|
||||
translations_path: Utf8PathBuf,
|
||||
branding: SiteBranding,
|
||||
) -> Result<Self, TemplateLoadingError> {
|
||||
let (translator, environment) = Self::load_(
|
||||
&path,
|
||||
url_builder.clone(),
|
||||
&vite_manifest_path,
|
||||
&translations_path,
|
||||
branding.clone(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
@ -166,6 +170,7 @@ impl Templates {
|
||||
url_builder,
|
||||
vite_manifest_path,
|
||||
translations_path,
|
||||
branding,
|
||||
})
|
||||
}
|
||||
|
||||
@ -174,6 +179,7 @@ impl Templates {
|
||||
url_builder: UrlBuilder,
|
||||
vite_manifest_path: &Utf8Path,
|
||||
translations_path: &Utf8Path,
|
||||
branding: SiteBranding,
|
||||
) -> Result<(Arc<Translator>, Arc<minijinja::Environment<'static>>), TemplateLoadingError> {
|
||||
let path = path.to_owned();
|
||||
let span = tracing::Span::current();
|
||||
@ -226,6 +232,8 @@ impl Templates {
|
||||
})
|
||||
.await??;
|
||||
|
||||
env.add_global("branding", Value::from_struct_object(branding));
|
||||
|
||||
self::functions::register(
|
||||
&mut env,
|
||||
url_builder,
|
||||
@ -259,6 +267,7 @@ impl Templates {
|
||||
self.url_builder.clone(),
|
||||
&self.vite_manifest_path,
|
||||
&self.translations_path,
|
||||
self.branding.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -409,11 +418,18 @@ mod tests {
|
||||
|
||||
let path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../templates/");
|
||||
let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
|
||||
let branding = SiteBranding::new("example.com").with_service_name("Example");
|
||||
let vite_manifest_path =
|
||||
Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../frontend/dist/manifest.json");
|
||||
let translations_path =
|
||||
Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../translations");
|
||||
let templates = Templates::load(path, url_builder, vite_manifest_path, translations_path)
|
||||
let templates = Templates::load(
|
||||
path,
|
||||
url_builder,
|
||||
vite_manifest_path,
|
||||
translations_path,
|
||||
branding,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
templates.check_render(now, &mut rng).unwrap();
|
||||
|
@ -8,6 +8,21 @@
|
||||
"secrets"
|
||||
],
|
||||
"properties": {
|
||||
"branding": {
|
||||
"description": "Configuration section for tweaking the branding of the service",
|
||||
"default": {
|
||||
"imprint": null,
|
||||
"logo_uri": null,
|
||||
"policy_uri": null,
|
||||
"service_name": null,
|
||||
"tos_uri": null
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/BrandingConfig"
|
||||
}
|
||||
]
|
||||
},
|
||||
"clients": {
|
||||
"description": "List of OAuth 2.0/OIDC clients config",
|
||||
"default": [],
|
||||
@ -299,6 +314,35 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"BrandingConfig": {
|
||||
"description": "Configuration section for tweaking the branding of the service",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"imprint": {
|
||||
"description": "Legal imprint, displayed in the footer in the footer of web pages and emails.",
|
||||
"type": "string"
|
||||
},
|
||||
"logo_uri": {
|
||||
"description": "Logo displayed in some web pages.",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"policy_uri": {
|
||||
"description": "Link to a privacy policy, displayed in the footer of web pages and emails. It is also advertised to clients through the `op_policy_uri` OIDC provider metadata.",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
"service_name": {
|
||||
"description": "A human-readable name. Defaults to the server's address.",
|
||||
"type": "string"
|
||||
},
|
||||
"tos_uri": {
|
||||
"description": "Link to a terms of service document, displayed in the footer of web pages and emails. It is also advertised to clients through the `op_tos_uri` OIDC provider metadata.",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ClaimsImports": {
|
||||
"description": "How claims should be imported",
|
||||
"type": "object",
|
||||
|
@ -23,7 +23,7 @@ limitations under the License.
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>matrix-authentication-service</title>
|
||||
<script type="application/javascript">
|
||||
window.APP_CONFIG = JSON.parse('{"root": "/account/", "graphqlEndpoint": "/graphql"}');
|
||||
window.APP_CONFIG = JSON.parse('{"root": "/account/", "graphqlEndpoint": "/graphql", "branding": {}}');
|
||||
(function () {
|
||||
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
function handleChange(list) {
|
||||
|
@ -5,6 +5,16 @@
|
||||
"continue": "Continue",
|
||||
"save": "Save"
|
||||
},
|
||||
"branding": {
|
||||
"privacy_policy": {
|
||||
"alt": "Link to the service privacy policy",
|
||||
"link": "Privacy Policy"
|
||||
},
|
||||
"terms_and_conditions": {
|
||||
"alt": "Link to the service terms and conditions",
|
||||
"link": "Terms & Conditions"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"add": "Add",
|
||||
"error": "Error",
|
||||
|
@ -13,27 +13,28 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
max-width: calc(378px + var(--cpd-space-6x) * 2);
|
||||
margin: 0 auto;
|
||||
padding: var(--cpd-space-6x);
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: var(--cpd-space-6x);
|
||||
padding: var(--cpd-space-6x) 0;
|
||||
border-top: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
margin: var(--cpd-space-4x) 0;
|
||||
|
||||
& > ul {
|
||||
.legal-footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: var(--cpd-space-2x);
|
||||
|
||||
font: var(--cpd-font-body-sm-regular);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-sm);
|
||||
|
||||
& nav {
|
||||
display: flex;
|
||||
gap: var(--cpd-space-2x);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
& .separator {
|
||||
color: var(--cpd-color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
& .imprint {
|
||||
color: var(--cpd-color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
54
frontend/src/components/Footer/Footer.stories.tsx
Normal file
54
frontend/src/components/Footer/Footer.stories.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
// 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.
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import Footer from "./Footer";
|
||||
|
||||
const meta = {
|
||||
title: "UI/Footer",
|
||||
component: Footer,
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof Footer>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Footer>;
|
||||
|
||||
export const Basic: Story = {
|
||||
args: {
|
||||
tosUri: "https://matrix.org/legal/terms-and-conditions/",
|
||||
policyUri: "https://matrix.org/legal/privacy-notice/",
|
||||
imprint: "The Matrix.org Foundation C.I.C.",
|
||||
},
|
||||
};
|
||||
|
||||
export const LinksOnly: Story = {
|
||||
args: {
|
||||
tosUri: "https://matrix.org/legal/terms-and-conditions/",
|
||||
policyUri: "https://matrix.org/legal/privacy-notice/",
|
||||
},
|
||||
};
|
||||
|
||||
export const ImprintOnly: Story = {
|
||||
args: {
|
||||
imprint: "The Matrix.org Foundation C.I.C.",
|
||||
},
|
||||
};
|
||||
|
||||
export const OneLink: Story = {
|
||||
args: {
|
||||
tosUri: "https://matrix.org/legal/terms-and-conditions/",
|
||||
imprint: "The Matrix.org Foundation C.I.C.",
|
||||
},
|
||||
};
|
57
frontend/src/components/Footer/Footer.tsx
Normal file
57
frontend/src/components/Footer/Footer.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
// 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.
|
||||
|
||||
import { Link } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import styles from "./Footer.module.css";
|
||||
|
||||
type Props = {
|
||||
policyUri?: string;
|
||||
tosUri?: string;
|
||||
imprint?: string;
|
||||
};
|
||||
|
||||
const Footer: React.FC<Props> = ({ policyUri, tosUri, imprint }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<footer className={styles.legalFooter}>
|
||||
{(policyUri || tosUri) && (
|
||||
<nav>
|
||||
{policyUri && (
|
||||
<Link href={policyUri} title={t("branding.privacy_policy.alt")}>
|
||||
{t("branding.privacy_policy.link")}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{policyUri && tosUri && (
|
||||
<div className={styles.separator} aria-hidden="true">
|
||||
•
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tosUri && (
|
||||
<Link href={tosUri} title={t("branding.terms_and_conditions.alt")}>
|
||||
{t("branding.terms_and_conditions.link")}
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{imprint && <p className={styles.imprint}>{imprint}</p>}
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
15
frontend/src/components/Footer/index.ts
Normal file
15
frontend/src/components/Footer/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// 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.
|
||||
|
||||
export { default } from "./Footer";
|
26
frontend/src/components/Layout/Layout.module.css
Normal file
26
frontend/src/components/Layout/Layout.module.css
Normal file
@ -0,0 +1,26 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
.layout-container {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
max-width: calc(420px + var(--cpd-space-6x) * 2);
|
||||
min-height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding: var(--cpd-space-6x);
|
||||
gap: var(--cpd-space-6x);
|
||||
}
|
@ -17,8 +17,8 @@
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi, afterAll, beforeEach } from "vitest";
|
||||
|
||||
import { currentUserIdAtom, GqlResult } from "../atoms";
|
||||
import { WithLocation } from "../test-utils/WithLocation";
|
||||
import { currentUserIdAtom, GqlResult } from "../../atoms";
|
||||
import { WithLocation } from "../../test-utils/WithLocation";
|
||||
|
||||
import Layout from "./Layout";
|
||||
|
@ -15,19 +15,21 @@
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { currentUserIdAtom } from "../atoms";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
import { routeAtom } from "../routing";
|
||||
import { currentUserIdAtom } from "../../atoms";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../../result";
|
||||
import { appConfigAtom, routeAtom } from "../../routing";
|
||||
import Footer from "../Footer";
|
||||
import GraphQLError from "../GraphQLError";
|
||||
import NavBar from "../NavBar";
|
||||
import NavItem from "../NavItem";
|
||||
import NotLoggedIn from "../NotLoggedIn";
|
||||
import UserGreeting from "../UserGreeting";
|
||||
|
||||
import GraphQLError from "./GraphQLError";
|
||||
import styles from "./Layout.module.css";
|
||||
import NavBar from "./NavBar";
|
||||
import NavItem from "./NavItem";
|
||||
import NotLoggedIn from "./NotLoggedIn";
|
||||
import UserGreeting from "./UserGreeting";
|
||||
|
||||
const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const route = useAtomValue(routeAtom);
|
||||
const appConfig = useAtomValue(appConfigAtom);
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -45,7 +47,7 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.layoutContainer}>
|
||||
{shouldHideNavBar ? null : (
|
||||
<>
|
||||
<UserGreeting userId={userId} />
|
||||
@ -63,18 +65,11 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
|
||||
<main>{children}</main>
|
||||
|
||||
{/* TODO: the footer needs to be reworked to show configurable info not hardcoded links: https://github.com/matrix-org/matrix-authentication-service/issues/1675 */}
|
||||
{/* <footer className={styles.footer}>
|
||||
<nav className={styles.footerLinks}>
|
||||
<ul>
|
||||
<Link href="https://matrix.org/legal/copyright-notice">Info</Link>
|
||||
<Link href="https://matrix.org/legal/privacy-notice">Privacy</Link>
|
||||
<Link href="https://matrix.org/legal/terms-and-conditions">
|
||||
Terms & Conditions
|
||||
</Link>
|
||||
</ul>
|
||||
</nav>
|
||||
</footer> */}
|
||||
<Footer
|
||||
imprint={appConfig.branding?.imprint}
|
||||
tosUri={appConfig.branding?.tosUri}
|
||||
policyUri={appConfig.branding?.policyUri}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
15
frontend/src/components/Layout/index.ts
Normal file
15
frontend/src/components/Layout/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
// 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.
|
||||
|
||||
export { default } from "./Layout";
|
@ -15,7 +15,6 @@
|
||||
|
||||
.nav-bar {
|
||||
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-gray-400);
|
||||
margin: var(--cpd-space-6x) 0;
|
||||
padding: 0 var(--cpd-space-10x);
|
||||
}
|
||||
|
||||
|
@ -13,10 +13,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.alert {
|
||||
margin-top: var(--cpd-space-4x);
|
||||
|
||||
> * {
|
||||
.alert > * {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
@ -15,6 +15,11 @@
|
||||
export type AppConfig = {
|
||||
root: string;
|
||||
graphqlEndpoint: string;
|
||||
branding?: {
|
||||
tosUri?: string;
|
||||
policyUri?: string;
|
||||
imprint?: string;
|
||||
};
|
||||
};
|
||||
|
||||
interface IWindow {
|
||||
@ -22,6 +27,10 @@ interface IWindow {
|
||||
}
|
||||
|
||||
const config: AppConfig = (typeof window !== "undefined" &&
|
||||
(window as IWindow).APP_CONFIG) || { root: "/", graphqlEndpoint: "/graphql" };
|
||||
(window as IWindow).APP_CONFIG) || {
|
||||
root: "/",
|
||||
graphqlEndpoint: "/graphql",
|
||||
branding: {},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -23,6 +23,9 @@
|
||||
@import "./styles/cpd-form.css";
|
||||
@import "./styles/cpd-link.css";
|
||||
|
||||
@import "./components/Layout/Layout.module.css";
|
||||
@import "./components/Footer/Footer.module.css";
|
||||
|
||||
@config "../tailwind.templates.config.cjs";
|
||||
|
||||
@tailwind base;
|
||||
@ -119,3 +122,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& hr {
|
||||
flex: 1;
|
||||
border: none;
|
||||
border-top: 1px solid var(--cpd-color-bg-subtle-primary);
|
||||
}
|
||||
|
||||
& p {
|
||||
margin-inline: var(--cpd-space-4x);
|
||||
text-transform: uppercase;
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
letter-spacing: var(--cpd-font-letter-spacing-body-md);
|
||||
color: var(--cpd-color-text-primary);
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,16 @@ limitations under the License.
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ _("app.name") }}</title>
|
||||
<script>
|
||||
window.APP_CONFIG = JSON.parse("{{ app_config | tojson | add_slashes | safe }}");
|
||||
{% set config = {
|
||||
'branding': {
|
||||
'imprint': branding.imprint,
|
||||
'tosUri': branding.tos_uri,
|
||||
'policyUri': branding.policy_uri,
|
||||
},
|
||||
'graphqlEndpoint': app_config.graphqlEndpoint,
|
||||
'root': app_config.root,
|
||||
} -%}
|
||||
window.APP_CONFIG = JSON.parse("{{ config | tojson | add_slashes | safe }}");
|
||||
(function () {
|
||||
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
function handleChange(list) {
|
||||
|
@ -20,7 +20,6 @@ limitations under the License.
|
||||
{% import "components/field.html" as field %}
|
||||
{% import "components/back_to_client.html" as back_to_client %}
|
||||
{% import "components/logout.html" as logout %}
|
||||
{% import "components/navbar.html" as navbar %}
|
||||
{% import "components/errors.html" as errors %}
|
||||
{% import "components/icon.html" as icon %}
|
||||
{% import "components/scope.html" as scope %}
|
||||
@ -33,7 +32,10 @@ limitations under the License.
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{{ include_asset('src/templates.css', preload=true) | indent(4) | safe }}
|
||||
</head>
|
||||
<body class="flex flex-col min-h-screen">
|
||||
<body>
|
||||
<div class="layout-container">
|
||||
{% block content %}{% endblock content %}
|
||||
{% include "components/footer.html" %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -55,3 +55,11 @@ limitations under the License.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro separator() %}
|
||||
<div class="separator">
|
||||
<hr />
|
||||
<p>{{ _("mas.or_separator") }}</p>
|
||||
<hr />
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
41
templates/components/footer.html
Normal file
41
templates/components/footer.html
Normal file
@ -0,0 +1,41 @@
|
||||
{#
|
||||
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.
|
||||
#}
|
||||
|
||||
<footer class="legal-footer">
|
||||
{%- if branding.policy_uri or branding.tos_uri -%}
|
||||
<nav>
|
||||
{%- if branding.policy_uri -%}
|
||||
<a href="{{ branding.policy_uri }}" referrerpolicy="no-referrer" title="{{ _('branding.privacy_policy.alt') }}" class="cpd-link" data-kind="primary">
|
||||
{{- _("branding.privacy_policy.link") -}}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if branding.policy_uri and branding.tos_uri -%}
|
||||
<div class="separator" aria-hidden="true">•</div>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if branding.tos_uri -%}
|
||||
<a href="{{ branding.tos_uri }}" referrerpolicy="no-referrer" title="{{ _('branding.terms_and_conditions.alt') }}" class="cpd-link" data-kind="primary">
|
||||
{{- _("branding.terms_and_conditions.link") -}}
|
||||
</a>
|
||||
{%- endif -%}
|
||||
</nav>
|
||||
{%- endif -%}
|
||||
|
||||
{%- if branding.imprint -%}
|
||||
<p class="imprint">{{ branding.imprint }}</p>
|
||||
{%- endif -%}
|
||||
</footer>
|
@ -15,12 +15,16 @@ limitations under the License.
|
||||
#}
|
||||
|
||||
{% macro button(text, csrf_token, as_link=false, post_logout_action={}) %}
|
||||
<form method="POST" action="{{ "/logout" | prefix_url }}" class="inline">
|
||||
<form method="POST" action="{{ "/logout" | prefix_url }}" class="inline-flex">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
{% for key, value in post_logout_action|items %}
|
||||
<input type="hidden" name="{{ key }}" value="{{ value }}" />
|
||||
{% endfor %}
|
||||
<button class="{% if as_link %}cpd-link{% else %}cpd-button{% endif %}" data-kind="critical" type="submit">{{ text }}</button>
|
||||
{% if as_link %}
|
||||
<button class="cpd-link flex-1" data-kind="critical" type="submit">{{ text }}</button>
|
||||
{% else %}
|
||||
<button class="cpd-button flex-1" data-kind="destructive" data-size="lg" type="submit">{{ text }}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endmacro %}
|
||||
|
||||
|
@ -1,35 +0,0 @@
|
||||
{#
|
||||
Copyright 2022 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.
|
||||
#}
|
||||
|
||||
{% macro top() %}
|
||||
<nav class="container mx-auto py-2 flex-initial flex items-center px-8" role="navigation" aria-label="main navigation">
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<div class="grid grid-flow-col auto-cols-max gap-4 place-items-center">
|
||||
{% if current_session %}
|
||||
<div class="text-grey-200 dark:text-grey-250 mx-2">
|
||||
{{ _("mas.navbar.signed_in_as", username=current_session.user.username) }}
|
||||
</div>
|
||||
|
||||
{{ button.link(text=_("mas.navbar.my_account"), href="/account/") }}
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token) }}
|
||||
{% else %}
|
||||
{{ button.link(text=_("action.sign_in"), href="/login") }}
|
||||
{{ button.link_outline(text=_("mas.navbar.register"), href="/register") }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
{% endmacro %}
|
@ -17,8 +17,7 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex-1 flex items-center justify-center">
|
||||
<div class="w-64 flex flex-col gap-2">
|
||||
<main class="w-96 flex-1 flex flex-col gap-2 justify-center">
|
||||
<h1 class="text-xl font-semibold">{{ _("mas.not_found.heading") }}</h1>
|
||||
<p>{{ _("mas.not_found.description") }}</p>
|
||||
<div>
|
||||
@ -32,6 +31,5 @@ limitations under the License.
|
||||
|
||||
{{ version }} 404 Not Found</pre>
|
||||
</code>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
@ -17,12 +17,8 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{{ navbar.top() }}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||
<div class="text-center">
|
||||
<h1 class="text-lg text-center font-medium">{{ _("mas.add_email.heading") }}</h1>
|
||||
</div>
|
||||
<form method="POST" class="flex-1 flex flex-col gap-6 justify-center">
|
||||
<h1 class="cpd-text-heading-xl-semibold">{{ _("mas.add_email.heading") }}</h1>
|
||||
|
||||
{% if form.errors is not empty %}
|
||||
{% for error in form.errors %}
|
||||
@ -35,5 +31,5 @@ limitations under the License.
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
{{ field.input(label=_("common.email_address"), name="email", type="email", form_state=form, autocomplete="email", required=true) }}
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</section>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
@ -17,11 +17,9 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{{ navbar.top() }}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||
<form method="POST" class="flex-1 flex flex-col gap-6 justify-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-lg text-center font-medium">{{ _("mas.verify_email.headline") }}</h1>
|
||||
<h1 class="cpd-text-heading-xl-semibold">{{ _("mas.verify_email.headline") }}</h1>
|
||||
<p>{{ _("mas.verify_email.description", email=email.email) }}</p>
|
||||
</div>
|
||||
|
||||
@ -37,5 +35,4 @@ limitations under the License.
|
||||
{{ field.input(label=_("mas.verify_email.code"), name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }}
|
||||
{{ button.button(text=_("action.submit")) }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
@ -17,16 +17,13 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{{ navbar.top() }}
|
||||
<section class="container mx-auto grid gap-4 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 py-2 px-8">
|
||||
<form class="rounded border-2 border-grey-50 dark:border-grey-450 p-4 grid gap-4 xl:grid-cols-2 grid-cols-1 place-content-start" method="POST">
|
||||
<h2 class="text-xl font-semibold xl:col-span-2">{{ _("mas.change_password.heading") }}</h2>
|
||||
<form class="flex-1 flex flex-col justify-center gap-6" method="POST">
|
||||
<h2 class="cpd-text-heading-xl-semibold">{{ _("mas.change_password.heading") }}</h2>
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
{{ field.input(label=_("mas.change_password.current"), name="current_password", type="password", autocomplete="current-password", class="xl:col-span-2") }}
|
||||
{{ field.input(label=_("mas.change_password.current"), name="current_password", type="password", autocomplete="current-password") }}
|
||||
{{ field.input(label=_("mas.change_password.new"), name="new_password", type="password", autocomplete="new-password") }}
|
||||
{{ field.input(label=_("mas.change_password.confirm"), name="new_password_confirm", type="password", autocomplete="new-password") }}
|
||||
{{ button.button(text=_("mas.change_password.change"), type="submit", class="xl:col-span-2 place-self-end") }}
|
||||
{{ button.button(text=_("mas.change_password.change"), type="submit") }}
|
||||
</form>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
|
@ -18,8 +18,7 @@ limitations under the License.
|
||||
|
||||
{% block content %}
|
||||
{% set client_name = client.client_name | default(client.client_id) %}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<div class="w-96 mx-2 my-8 flex flex-col gap-6">
|
||||
<main class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2 text-center">
|
||||
{% if client.logo_uri %}
|
||||
<img class="consent-client-icon image" referrerpolicy="no-referrer" src="{{ client.logo_uri }}" />
|
||||
@ -71,6 +70,5 @@ limitations under the License.
|
||||
{{ _("mas.not_you", username=current_session.user.username) }}
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
@ -20,8 +20,7 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex-1 flex items-center justify-center">
|
||||
<div class="w-64 flex flex-col gap-2">
|
||||
<main class="flex flex-col gap-2">
|
||||
<h1 class="text-xl font-semibold">{{ _("error.unexpected") }}</h1>
|
||||
{% if code %}
|
||||
<p class="font-semibold font-mono">
|
||||
@ -39,6 +38,5 @@ limitations under the License.
|
||||
<pre class="font-mono whitespace-pre-wrap break-all">{{ details }}</pre>
|
||||
</code>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
@ -17,13 +17,22 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{{ navbar.top() }}
|
||||
<section class="flex-1 flex flex-col items-center justify-center">
|
||||
<div class="my-2 mx-8">
|
||||
<h1 class="my-2 text-5xl font-semibold leading-tight">{{ _("app.human_name") }}</h1>
|
||||
<p class="text-lg">
|
||||
<main class="flex-1 flex flex-col justify-center gap-6">
|
||||
<h1 class="cpd-text-heading-xl-semibold">{{ _("app.human_name") }}</h1>
|
||||
<p class="cpd-text-body-md-regular">
|
||||
{{ _("app.technical_description", discovery_url=discovery_url) }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if current_session %}
|
||||
<p class="cpd-text-body-md-regular">
|
||||
{{ _("mas.navbar.signed_in_as", username=current_session.user.username) }}
|
||||
</p>
|
||||
|
||||
{{ button.link(text=_("mas.navbar.my_account"), href="/account/") }}
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token) }}
|
||||
{% else %}
|
||||
{{ button.link(text=_("action.sign_in"), href="/login") }}
|
||||
{{ button.link_outline(text=_("mas.navbar.register"), href="/register") }}
|
||||
{% endif %}
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
@ -17,8 +17,7 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||
<div class="flex-1 flex flex-col gap-6 justify-center">
|
||||
{% if not password_disabled %}
|
||||
{% if next and next.kind == "link_upstream" %}
|
||||
<div class="text-center">
|
||||
@ -32,6 +31,7 @@ limitations under the License.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" class="flex flex-col gap-6">
|
||||
{% if form.errors is not empty %}
|
||||
{% for error in form.errors %}
|
||||
<div class="text-critical font-medium">
|
||||
@ -43,22 +43,8 @@ limitations under the License.
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
{{ field.input(label=_("common.username"), name="username", form_state=form, autocomplete="username", autocorrect="off", autocapitalize="none") }}
|
||||
{{ field.input(label=_("common.password"), name="password", type="password", form_state=form, autocomplete="password") }}
|
||||
{% if next and next.kind == "continue_authorization_grant" %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ back_to_client.link(
|
||||
text=_("action.cancel"),
|
||||
kind="destructive",
|
||||
uri=next.grant.redirect_uri,
|
||||
mode=next.grant.response_mode,
|
||||
params=dict(error="access_denied", state=next.grant.state)
|
||||
) }}
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if not next or next.kind != "link_upstream" %}
|
||||
<div class="text-center mt-4">
|
||||
@ -71,16 +57,12 @@ limitations under the License.
|
||||
|
||||
{% if providers %}
|
||||
{% if not password_disabled %}
|
||||
<div class="flex items-center">
|
||||
<hr class="flex-1" />
|
||||
<div class="mx-2">{{ _("mas.or_separator") }}</div>
|
||||
<hr class="flex-1" />
|
||||
</div>
|
||||
{{ field.separator() }}
|
||||
{% endif %}
|
||||
|
||||
{% for provider in providers %}
|
||||
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
|
||||
{{ button.link(text=_("mas.login.continue_with_provider", provider=provider.issuer), href="/upstream/authorize/" ~ provider.id ~ params) }}
|
||||
<a class="cpd-button" data-kind="secondary" data-size="lg" href="{{ ('/upstream/authorize/' ~ provider.id ~ params) | prefix_url }}">{{ _("mas.login.continue_with_provider", provider=provider.issuer | simplify_url) }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@ -89,6 +71,15 @@ limitations under the License.
|
||||
{{ _("mas.login.no_login_methods") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if next and next.kind == "continue_authorization_grant" %}
|
||||
{{ back_to_client.link(
|
||||
text=_("action.cancel"),
|
||||
kind="destructive",
|
||||
uri=next.grant.redirect_uri,
|
||||
mode=next.grant.response_mode,
|
||||
params=dict(error="access_denied", state=next.grant.state)
|
||||
) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -17,26 +17,24 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<div class="w-96 my-2 mx-8">
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<h1 class="text-xl font-semibold">{{ _("mas.policy_violation.heading") }}</h1>
|
||||
<section class="flex-1 flex flex-col gap-10 justify-center">
|
||||
<h1 class="cpd-text-heading-xl-semibold">{{ _("mas.policy_violation.heading") }}</h1>
|
||||
<p>{{ _("mas.policy_violation.description") }}</p>
|
||||
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex items-center">
|
||||
<div class="bg-white rounded w-16 h-16 overflow-hidden mx-auto">
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<div class="bg-white rounded w-16 h-16 overflow-hidden">
|
||||
{% if client.logo_uri %}
|
||||
<img referrerpolicy="no-referrer" class="w-16 h-16" src="{{ client.logo_uri }}" />
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1 class="text-lg text-center font-medium flex-1"><a target="_blank" href="{{ client.client_uri }}" class="cpd-link" data-kind="primary">{{ client.client_name | default(client.client_id) }}</a></h1>
|
||||
<a target="_blank" href="{{ client.client_uri }}" class="cpd-link" data-kind="primary">{{ client.client_name | default(client.client_id) }}</a>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex items-center">
|
||||
<div class="text-center flex-1">
|
||||
<div class="flex gap-4 justify-center items-center">
|
||||
<p>
|
||||
{{ _("mas.policy_violation.logged_as", username=current_session.user.username) }}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action) }}
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=True) }}
|
||||
</div>
|
||||
|
||||
{{ back_to_client.link(
|
||||
@ -46,8 +44,6 @@ limitations under the License.
|
||||
mode=grant.response_mode,
|
||||
params=dict(error="access_denied", state=grant.state)
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
|
@ -17,18 +17,20 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<div class="w-96 my-2 mx-8">
|
||||
<form method="POST" class="grid grid-cols-1 gap-6">
|
||||
<section class="flex-1 flex flex-col gap-6 justify-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-lg text-center font-medium">Hi {{ current_session.user.username }}</h1>
|
||||
<p>To continue, please verify it's you:</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="flex flex-col gap-6">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
{# TODO: errors #}
|
||||
{{ field.input(label=_("common.password"), name="password", type="password", form_state=form, autocomplete="password") }}
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</form>
|
||||
|
||||
{% if next and next.kind == "continue_authorization_grant" %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ back_to_client.link(
|
||||
text="Cancel",
|
||||
kind="destructive",
|
||||
@ -36,20 +38,13 @@ limitations under the License.
|
||||
mode=next.grant.response_mode,
|
||||
params=dict(error="access_denied", state=next.grant.state)
|
||||
) }}
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
<div class="text-center mt-4">
|
||||
|
||||
<div class="text-center">
|
||||
Not {{ current_session.user.username }}?
|
||||
{% set post_logout_action = next["params"] | default({}) %}
|
||||
{{ logout.button(text="Sign out", csrf_token=csrf_token, post_logout_action=post_logout_action, as_link=true) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
|
@ -17,12 +17,13 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<form method="POST" class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||
<section class="flex-1 flex flex-col gap-6 justify-center">
|
||||
<div class="text-center">
|
||||
<h1 class="text-lg text-center font-medium">{{ _("mas.register.create_account.heading") }}</h1>
|
||||
<p>{{ _("mas.register.create_account.description") }}</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" class="flex flex-col gap-6">
|
||||
{% if form.errors is not empty %}
|
||||
{% for error in form.errors %}
|
||||
<div class="text-critical font-medium">
|
||||
@ -36,9 +37,10 @@ limitations under the License.
|
||||
{{ field.input(label=_("common.email_address"), name="email", type="email", form_state=form, autocomplete="email") }}
|
||||
{{ field.input(label=_("common.password"), name="password", type="password", form_state=form, autocomplete="new-password") }}
|
||||
{{ field.input(label=_("common.password_confirm"), name="password_confirm", type="password", form_state=form, autocomplete="new-password") }}
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</form>
|
||||
|
||||
{% if next and next.kind == "continue_authorization_grant" %}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{{ back_to_client.link(
|
||||
text=_("action.cancel"),
|
||||
kind="destructive",
|
||||
@ -46,19 +48,12 @@ limitations under the License.
|
||||
mode=next.grant.response_mode,
|
||||
params=dict(error="access_denied", state=next.grant.state)
|
||||
) }}
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="text-center mt-4">
|
||||
{{ _("mas.register.call_to_login") }}
|
||||
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
|
||||
{{ button.link_text(text=_("mas.register.sign_in_instead"), href="/login" ~ params) }}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% endblock content %}
|
||||
|
@ -18,8 +18,7 @@ limitations under the License.
|
||||
|
||||
{% block content %}
|
||||
{% set client_name = login.redirect_uri | simplify_url %}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<div class="w-96 mx-2 my-8 flex flex-col gap-6">
|
||||
<main class="flex-1 flex flex-col gap-6 justify-center">
|
||||
<div class="flex flex-col gap-2 text-center">
|
||||
<div class="consent-client-icon generic">
|
||||
{{ icon.web_browser() }}
|
||||
@ -46,6 +45,5 @@ limitations under the License.
|
||||
{{ _("mas.not_you", username=current_session.user.username) }}
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=true) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
@ -17,10 +17,8 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<div class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||
<form method="POST" class="grid grid-cols-1 gap-6">
|
||||
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 text-center font-medium text-lg">
|
||||
<section class="flex-1 flex flex-col justify-center gap-6">
|
||||
<h1 class="cpd-text-heading-xl-semibold text-center">
|
||||
{% if force_localpart %}
|
||||
{{ _("mas.upstream_oauth2.register.create_account") }}
|
||||
{% else %}
|
||||
@ -28,6 +26,7 @@ limitations under the License.
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
<form method="POST" class="flex flex-col gap-6">
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="action" value="register" />
|
||||
{% if force_localpart %}
|
||||
@ -69,12 +68,7 @@ limitations under the License.
|
||||
|
||||
{{ button.button(text=_("action.create_account")) }}
|
||||
</form>
|
||||
<div class="flex items-center">
|
||||
<hr class="flex-1" />
|
||||
<div class="mx-2">{{ _("mas.or_separator") }}</div>
|
||||
<hr class="flex-1" />
|
||||
</div>
|
||||
{{ field.separator() }}
|
||||
{{ button.link_outline(text=_("mas.upstream_oauth2.register.link_existing"), href=login_link) }}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
@ -17,14 +17,11 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{{ navbar.top() }}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<div class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col font-medium text-lg text-center">
|
||||
<section class="flex-1 flex flex-col gap-6 justify-center">
|
||||
<h1 class="cpd-text-heading-xl-semibold text-center">
|
||||
{{ _("mas.upstream_oauth2.link_mismatch.heading") }}
|
||||
</h1>
|
||||
|
||||
<div>{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token) }}</div>
|
||||
</div>
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token) }}
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
@ -17,10 +17,8 @@ limitations under the License.
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
{{ navbar.top() }}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<div class="grid grid-cols-1 gap-6 w-96 my-2 mx-8">
|
||||
<h1 class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col font-medium text-lg text-center">
|
||||
<section class="flex-1 flex flex-col gap-6 justify-center">
|
||||
<h1 class="cpd-text-heading-xl-semibold text-center">
|
||||
{{ _("mas.upstream_oauth2.suggest_link.heading") }}
|
||||
</h1>
|
||||
|
||||
@ -31,13 +29,8 @@ limitations under the License.
|
||||
{{ button.button(text=_("mas.upstream_oauth2.suggest_link.action"), class="flex-1") }}
|
||||
</form>
|
||||
|
||||
<div class="flex items-center">
|
||||
<hr class="flex-1" />
|
||||
<div class="mx-2">{{ _("mas.or_separator") }}</div>
|
||||
<hr class="flex-1" />
|
||||
</div>
|
||||
{{ field.separator() }}
|
||||
|
||||
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=post_logout_action) }}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
@ -2,68 +2,90 @@
|
||||
"action": {
|
||||
"cancel": "Cancel",
|
||||
"@cancel": {
|
||||
"context": "pages/consent.html:63:13-31, pages/login.html:49:19-37, pages/policy_violation.html:43:15-33, pages/register.html:43:17-35"
|
||||
"context": "pages/consent.html:62:11-29, pages/login.html:77:13-31, pages/policy_violation.html:41:11-29, pages/register.html:45:15-33"
|
||||
},
|
||||
"continue": "Continue",
|
||||
"@continue": {
|
||||
"context": "pages/account/emails/add.html:37:28-48, pages/consent.html:59:30-50, pages/login.html:55:34-54, pages/login.html:59:34-54, pages/reauth.html:39:34-54, pages/reauth.html:43:34-54, pages/register.html:49:32-52, pages/register.html:53:32-52, pages/sso.html:42:30-50"
|
||||
"context": "pages/account/emails/add.html:33:26-46, pages/consent.html:58:28-48, pages/login.html:46:30-50, pages/reauth.html:30:28-48, pages/register.html:40:28-48, pages/sso.html:41:28-48"
|
||||
},
|
||||
"create_account": "Create Account",
|
||||
"@create_account": {
|
||||
"context": "pages/login.html:67:37-63, pages/upstream_oauth2/do_register.html:70:30-56"
|
||||
"context": "pages/login.html:53:35-61, pages/upstream_oauth2/do_register.html:69:28-54"
|
||||
},
|
||||
"sign_in": "Sign in",
|
||||
"@sign_in": {
|
||||
"context": "components/navbar.html:30:28-47"
|
||||
"context": "pages/index.html:34:26-45"
|
||||
},
|
||||
"sign_out": "Sign out",
|
||||
"@sign_out": {
|
||||
"context": "components/navbar.html:28:30-50, pages/consent.html:72:30-50, pages/policy_violation.html:39:32-52, pages/sso.html:47:30-50, pages/upstream_oauth2/link_mismatch.html:27:33-53, pages/upstream_oauth2/suggest_link.html:40:28-48"
|
||||
"context": "pages/consent.html:71:28-48, pages/index.html:32:28-48, pages/policy_violation.html:37:28-48, pages/sso.html:46:28-48, pages/upstream_oauth2/link_mismatch.html:25:26-46, pages/upstream_oauth2/suggest_link.html:34:28-48"
|
||||
},
|
||||
"submit": "Submit",
|
||||
"@submit": {
|
||||
"context": "pages/account/emails/verify.html:38:28-46"
|
||||
"context": "pages/account/emails/verify.html:36:26-44"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"human_name": "Matrix Authentication Service",
|
||||
"@human_name": {
|
||||
"context": "pages/index.html:23:63-82",
|
||||
"context": "pages/index.html:21:48-67",
|
||||
"description": "Human readable name of the application"
|
||||
},
|
||||
"name": "matrix-authentication-service",
|
||||
"@name": {
|
||||
"context": "app.html:25:14-27, base.html:32:31-44",
|
||||
"context": "app.html:25:14-27, base.html:31:31-44",
|
||||
"description": "Name of the application"
|
||||
},
|
||||
"technical_description": "OpenID Connect discovery document: <a class=\"cpd-link\" data-kind=\"primary\" href=\"%(discovery_url)s\">%(discovery_url)s</a>",
|
||||
"@technical_description": {
|
||||
"context": "pages/index.html:25:11-70",
|
||||
"context": "pages/index.html:23:9-68",
|
||||
"description": "Introduction text displayed on the home page"
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
"privacy_policy": {
|
||||
"alt": "Link to the service privacy policy",
|
||||
"@alt": {
|
||||
"context": "components/footer.html:21:83-115"
|
||||
},
|
||||
"link": "Privacy Policy",
|
||||
"@link": {
|
||||
"context": "components/footer.html:22:14-47"
|
||||
}
|
||||
},
|
||||
"terms_and_conditions": {
|
||||
"alt": "Link to the service terms and conditions",
|
||||
"@alt": {
|
||||
"context": "components/footer.html:31:80-118"
|
||||
},
|
||||
"link": "Terms & Conditions",
|
||||
"@link": {
|
||||
"context": "components/footer.html:32:14-53"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"email_address": "Email address",
|
||||
"@email_address": {
|
||||
"context": "pages/account/emails/add.html:36:27-52, pages/register.html:36:27-52"
|
||||
"context": "pages/account/emails/add.html:32:25-50, pages/register.html:37:27-52"
|
||||
},
|
||||
"password": "Password",
|
||||
"@password": {
|
||||
"context": "pages/login.html:45:29-49, pages/reauth.html:29:29-49, pages/register.html:37:27-47"
|
||||
"context": "pages/login.html:45:29-49, pages/reauth.html:29:27-47, pages/register.html:38:27-47"
|
||||
},
|
||||
"password_confirm": "Confirm password",
|
||||
"@password_confirm": {
|
||||
"context": "pages/register.html:38:27-55"
|
||||
"context": "pages/register.html:39:27-55"
|
||||
},
|
||||
"username": "Username",
|
||||
"@username": {
|
||||
"context": "pages/login.html:44:29-49, pages/register.html:35:27-47, pages/upstream_oauth2/do_register.html:39:31-51"
|
||||
"context": "pages/login.html:44:29-49, pages/register.html:36:27-47, pages/upstream_oauth2/do_register.html:38:29-49"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unexpected": "Unexpected error",
|
||||
"@unexpected": {
|
||||
"context": "pages/error.html:25:41-62",
|
||||
"context": "pages/error.html:24:41-62",
|
||||
"description": "Error message displayed when an unexpected error occurs"
|
||||
}
|
||||
},
|
||||
@ -71,38 +93,38 @@
|
||||
"add_email": {
|
||||
"heading": "Add an email address",
|
||||
"@heading": {
|
||||
"context": "pages/account/emails/add.html:24:55-81",
|
||||
"context": "pages/account/emails/add.html:21:48-74",
|
||||
"description": "Heading for the page to add an email address"
|
||||
}
|
||||
},
|
||||
"back_to_homepage": "Go back to the homepage",
|
||||
"@back_to_homepage": {
|
||||
"context": "pages/404.html:25:37-62"
|
||||
"context": "pages/404.html:24:29-54"
|
||||
},
|
||||
"change_password": {
|
||||
"change": "Change password",
|
||||
"@change": {
|
||||
"context": "pages/account/password.html:28:28-59",
|
||||
"context": "pages/account/password.html:26:26-57",
|
||||
"description": "Button to change the user's password"
|
||||
},
|
||||
"confirm": "Confirm password",
|
||||
"@confirm": {
|
||||
"context": "pages/account/password.html:27:27-59",
|
||||
"context": "pages/account/password.html:25:25-57",
|
||||
"description": "Confirmation field for the new password"
|
||||
},
|
||||
"current": "Current password",
|
||||
"@current": {
|
||||
"context": "pages/account/password.html:25:27-59",
|
||||
"context": "pages/account/password.html:23:25-57",
|
||||
"description": "Field for the user's current password"
|
||||
},
|
||||
"heading": "Change my password",
|
||||
"@heading": {
|
||||
"context": "pages/account/password.html:23:57-89",
|
||||
"context": "pages/account/password.html:21:48-80",
|
||||
"description": "Heading on the change password page"
|
||||
},
|
||||
"new": "New password",
|
||||
"@new": {
|
||||
"context": "pages/account/password.html:26:27-55",
|
||||
"context": "pages/account/password.html:24:25-53",
|
||||
"description": "Field for the user's new password"
|
||||
}
|
||||
},
|
||||
@ -155,106 +177,106 @@
|
||||
"login": {
|
||||
"call_to_register": "Don't have an account yet?",
|
||||
"@call_to_register": {
|
||||
"context": "pages/login.html:65:15-46"
|
||||
"context": "pages/login.html:51:13-44"
|
||||
},
|
||||
"continue_with_provider": "Continue with %(provider)s",
|
||||
"@continue_with_provider": {
|
||||
"context": "pages/login.html:83:30-93",
|
||||
"context": "pages/login.html:65:144-222",
|
||||
"description": "Button to log in with an upstream provider"
|
||||
},
|
||||
"description": "Please sign in to continue:",
|
||||
"@description": {
|
||||
"context": "pages/login.html:31:18-44"
|
||||
"context": "pages/login.html:30:16-42"
|
||||
},
|
||||
"headline": "Sign in",
|
||||
"@headline": {
|
||||
"context": "pages/login.html:30:59-82"
|
||||
"context": "pages/login.html:29:57-80"
|
||||
},
|
||||
"link": {
|
||||
"description": "Linking your <span class=\"break-keep text-links\">%(provider)s</span> account",
|
||||
"@description": {
|
||||
"context": "pages/login.html:26:34-96"
|
||||
"context": "pages/login.html:25:32-94"
|
||||
},
|
||||
"headline": "Sign in to link",
|
||||
"@headline": {
|
||||
"context": "pages/login.html:25:59-87"
|
||||
"context": "pages/login.html:24:57-85"
|
||||
}
|
||||
},
|
||||
"no_login_methods": "No login methods available.",
|
||||
"@no_login_methods": {
|
||||
"context": "pages/login.html:89:13-44"
|
||||
"context": "pages/login.html:71:11-42"
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
"my_account": "My account",
|
||||
"@my_account": {
|
||||
"context": "components/navbar.html:27:28-54"
|
||||
"context": "pages/index.html:31:26-52"
|
||||
},
|
||||
"register": "Create an account",
|
||||
"@register": {
|
||||
"context": "components/navbar.html:31:36-60"
|
||||
"context": "pages/index.html:35:34-58"
|
||||
},
|
||||
"signed_in_as": "Signed in as <span class=\"font-semibold\">%(username)s</span>.",
|
||||
"@signed_in_as": {
|
||||
"context": "components/navbar.html:24:13-81",
|
||||
"context": "pages/index.html:28:11-79",
|
||||
"description": "Displayed in the navbar when the user is signed in"
|
||||
}
|
||||
},
|
||||
"not_found": {
|
||||
"description": "The page you were looking for doesn't exist or has been moved",
|
||||
"@description": {
|
||||
"context": "pages/404.html:23:14-44"
|
||||
"context": "pages/404.html:22:8-38"
|
||||
},
|
||||
"heading": "Page not found",
|
||||
"@heading": {
|
||||
"context": "pages/404.html:22:45-71"
|
||||
"context": "pages/404.html:21:39-65"
|
||||
}
|
||||
},
|
||||
"not_you": "Not %(username)s?",
|
||||
"@not_you": {
|
||||
"context": "pages/consent.html:71:11-67, pages/sso.html:46:11-67",
|
||||
"context": "pages/consent.html:70:9-65, pages/sso.html:45:9-65",
|
||||
"description": "Suggestions for the user to log in as a different user"
|
||||
},
|
||||
"or_separator": "Or",
|
||||
"@or_separator": {
|
||||
"context": "pages/login.html:76:33-54, pages/upstream_oauth2/do_register.html:74:29-50, pages/upstream_oauth2/suggest_link.html:36:29-50",
|
||||
"context": "components/field.html:62:10-31",
|
||||
"description": "Separator between the login methods"
|
||||
},
|
||||
"policy_violation": {
|
||||
"description": "This might be because of the client which authored the request, the currently logged in user, or the request itself.",
|
||||
"@description": {
|
||||
"context": "pages/policy_violation.html:24:14-51",
|
||||
"context": "pages/policy_violation.html:22:10-47",
|
||||
"description": "Displayed when an authorization request is denied by the policy"
|
||||
},
|
||||
"heading": "The authorization request was denied the policy enforced by this service",
|
||||
"@heading": {
|
||||
"context": "pages/policy_violation.html:23:45-78",
|
||||
"context": "pages/policy_violation.html:21:48-81",
|
||||
"description": "Displayed when an authorization request is denied by the policy"
|
||||
},
|
||||
"logged_as": "Logged as <span class=\"font-semibold\">%(username)s</span>",
|
||||
"@logged_as": {
|
||||
"context": "pages/policy_violation.html:36:15-90"
|
||||
"context": "pages/policy_violation.html:34:11-86"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"call_to_login": "Already have an account?",
|
||||
"@call_to_login": {
|
||||
"context": "pages/register.html:57:11-42",
|
||||
"context": "pages/register.html:54:9-40",
|
||||
"description": "Displayed on the registration page to suggest to log in instead"
|
||||
},
|
||||
"create_account": {
|
||||
"description": "Please create an account to get started:",
|
||||
"@description": {
|
||||
"context": "pages/register.html:24:14-58"
|
||||
"context": "pages/register.html:23:12-56"
|
||||
},
|
||||
"heading": "Create an account",
|
||||
"@heading": {
|
||||
"context": "pages/register.html:23:55-95"
|
||||
"context": "pages/register.html:22:53-93"
|
||||
}
|
||||
},
|
||||
"sign_in_instead": "Sign in instead",
|
||||
"@sign_in_instead": {
|
||||
"context": "pages/register.html:59:33-66"
|
||||
"context": "pages/register.html:56:31-64"
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
@ -297,75 +319,75 @@
|
||||
"link_mismatch": {
|
||||
"heading": "This upstream account is already linked to another account.",
|
||||
"@heading": {
|
||||
"context": "pages/upstream_oauth2/link_mismatch.html:24:11-57",
|
||||
"context": "pages/upstream_oauth2/link_mismatch.html:22:9-55",
|
||||
"description": "Page shown when the user tries to link an upstream account that is already linked to another account"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"choose_username": "Choose your username",
|
||||
"@choose_username": {
|
||||
"context": "pages/upstream_oauth2/do_register.html:27:15-64",
|
||||
"context": "pages/upstream_oauth2/do_register.html:25:11-60",
|
||||
"description": "Displayed when creating a new account from an SSO login, and the username is not forced"
|
||||
},
|
||||
"create_account": "Create a new account",
|
||||
"@create_account": {
|
||||
"context": "pages/upstream_oauth2/do_register.html:25:15-63",
|
||||
"context": "pages/upstream_oauth2/do_register.html:23:11-59",
|
||||
"description": "Displayed when creating a new account from an SSO login, and the username is pre-filled and forced"
|
||||
},
|
||||
"forced_display_name": "Will use the following display name",
|
||||
"@forced_display_name": {
|
||||
"context": "pages/upstream_oauth2/do_register.html:60:19-72",
|
||||
"context": "pages/upstream_oauth2/do_register.html:59:17-70",
|
||||
"description": "Tells the user what display name will be imported"
|
||||
},
|
||||
"forced_email": "Will use the following email address",
|
||||
"@forced_email": {
|
||||
"context": "pages/upstream_oauth2/do_register.html:46:19-65",
|
||||
"context": "pages/upstream_oauth2/do_register.html:45:17-63",
|
||||
"description": "Tells the user which email address will be imported"
|
||||
},
|
||||
"forced_localpart": "Will use the following username",
|
||||
"@forced_localpart": {
|
||||
"context": "pages/upstream_oauth2/do_register.html:35:41-91",
|
||||
"context": "pages/upstream_oauth2/do_register.html:34:39-89",
|
||||
"description": "Tells the user which username will be used"
|
||||
},
|
||||
"link_existing": "Link to an existing account",
|
||||
"@link_existing": {
|
||||
"context": "pages/upstream_oauth2/do_register.html:77:34-81",
|
||||
"context": "pages/upstream_oauth2/do_register.html:72:32-79",
|
||||
"description": "Button to link an existing account after an SSO login"
|
||||
},
|
||||
"suggested_display_name": "Import display name",
|
||||
"@suggested_display_name": {
|
||||
"context": "pages/upstream_oauth2/do_register.html:63:50-106",
|
||||
"context": "pages/upstream_oauth2/do_register.html:62:48-104",
|
||||
"description": "Option to let the user import their display name after an SSO login"
|
||||
},
|
||||
"suggested_email": "Import email address",
|
||||
"@suggested_email": {
|
||||
"context": "pages/upstream_oauth2/do_register.html:49:45-94",
|
||||
"context": "pages/upstream_oauth2/do_register.html:48:43-92",
|
||||
"description": "Option to let the user import their email address after an SSO login"
|
||||
}
|
||||
},
|
||||
"suggest_link": {
|
||||
"action": "Link",
|
||||
"@action": {
|
||||
"context": "pages/upstream_oauth2/suggest_link.html:31:30-74"
|
||||
"context": "pages/upstream_oauth2/suggest_link.html:29:30-74"
|
||||
},
|
||||
"heading": "Link to your existing account",
|
||||
"@heading": {
|
||||
"context": "pages/upstream_oauth2/suggest_link.html:24:11-56"
|
||||
"context": "pages/upstream_oauth2/suggest_link.html:22:11-56"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verify_email": {
|
||||
"code": "Code",
|
||||
"@code": {
|
||||
"context": "pages/account/emails/verify.html:37:27-53"
|
||||
"context": "pages/account/emails/verify.html:35:25-51"
|
||||
},
|
||||
"description": "Please enter the 6-digit code sent to: <span class=\"font-semibold\">%(email)s</span>",
|
||||
"@description": {
|
||||
"context": "pages/account/emails/verify.html:25:14-66"
|
||||
"context": "pages/account/emails/verify.html:23:12-64"
|
||||
},
|
||||
"headline": "Email verification",
|
||||
"@headline": {
|
||||
"context": "pages/account/emails/verify.html:24:55-85"
|
||||
"context": "pages/account/emails/verify.html:22:50-80"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user