1
0
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:
Quentin Gliech
2023-10-24 19:02:28 +02:00
parent 10e31f03fa
commit 8984cc703b
50 changed files with 1077 additions and 604 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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",
])
}
}

View File

@ -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,13 +418,20 @@ 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)
.await
.unwrap();
let templates = Templates::load(
path,
url_builder,
vite_manifest_path,
translations_path,
branding,
)
.await
.unwrap();
templates.check_render(now, &mut rng).unwrap();
}
}

View File

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

View File

@ -23,7 +23,7 @@ limitations under the License.
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>matrix-authentication-service</title>
<script 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) {

View File

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

View File

@ -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);
}
.legal-footer {
display: flex;
flex-direction: column;
gap: var(--cpd-space-2x);
.footer {
margin-top: var(--cpd-space-6x);
padding: var(--cpd-space-6x) 0;
border-top: 1px solid var(--cpd-color-border-interactive-secondary);
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;
}
.footer-links {
margin: var(--cpd-space-4x) 0;
& > ul {
display: flex;
flex-direction: row;
justify-content: center;
gap: var(--cpd-space-2x);
& .separator {
color: var(--cpd-color-text-secondary);
}
}
& .imprint {
color: var(--cpd-color-text-secondary);
text-align: center;
}
}

View 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.",
},
};

View 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;

View 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";

View 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);
}

View File

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

View File

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

View 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";

View File

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

View File

@ -13,10 +13,6 @@
* limitations under the License.
*/
.alert {
margin-top: var(--cpd-space-4x);
> * {
box-sizing: content-box;
}
.alert > * {
box-sizing: content-box;
}

View File

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

View File

@ -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;
@ -30,92 +33,111 @@
@tailwind utilities;
.cpd-text-body-lg-regular {
font: var(--cpd-font-body-lg-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-lg);
font: var(--cpd-font-body-lg-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-lg);
}
.cpd-text-heading-xl-semibold {
font: var(--cpd-font-heading-xl-semibold);
letter-spacing: var(--cpd-font-letter-spacing-heading-xl);
font: var(--cpd-font-heading-xl-semibold);
letter-spacing: var(--cpd-font-letter-spacing-heading-xl);
}
.cpd-text-body-md-regular {
font: var(--cpd-font-body-md-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
font: var(--cpd-font-body-md-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
}
.cpd-text-primary {
color: var(--cpd-color-text-primary);
color: var(--cpd-color-text-primary);
}
.cpd-text-secondary {
color: var(--cpd-color-text-secondary);
color: var(--cpd-color-text-secondary);
}
.consent-client-icon {
display: block;
display: block;
height: var(--cpd-space-16x);
width: var(--cpd-space-16x);
margin: 0 auto;
&.generic {
background-color: var(--cpd-color-bg-subtle-secondary);
border-radius: var(--cpd-radius-pill-effect);
color: var(--cpd-color-icon-primary);
& svg {
margin: var(--cpd-space-4x);
height: var(--cpd-space-8x);
width: var(--cpd-space-8x);
}
}
&.image {
height: var(--cpd-space-16x);
width: var(--cpd-space-16x);
margin: 0 auto;
&.generic {
background-color: var(--cpd-color-bg-subtle-secondary);
border-radius: var(--cpd-radius-pill-effect);
color: var(--cpd-color-icon-primary);
& svg {
margin: var(--cpd-space-4x);
height: var(--cpd-space-8x);
width: var(--cpd-space-8x);
}
}
&.image {
height: var(--cpd-space-16x);
width: var(--cpd-space-16x);
border-radius: var(--cpd-space-2x);
overflow: hidden;
}
border-radius: var(--cpd-space-2x);
overflow: hidden;
}
}
.consent-scope-list {
--border-radius: var(--cpd-space-4x);
& ul {
display: flex;
flex-direction: column;
gap: var(--cpd-space-1x);
--border-radius: var(--cpd-space-4x);
& ul {
display: flex;
flex-direction: column;
gap: var(--cpd-space-1x);
& > li {
font: var(--cpd-font-body-md-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
color: var(--cpd-color-text-primary);
& > li {
font: var(--cpd-font-body-md-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
color: var(--cpd-color-text-primary);
background-color: var(--cpd-color-bg-subtle-secondary);
padding: var(--cpd-space-3x) var(--cpd-space-5x);
display: flex;
gap: var(--cpd-space-4x);
line-height: var(--cpd-space-6x);
background-color: var(--cpd-color-bg-subtle-secondary);
padding: var(--cpd-space-3x) var(--cpd-space-5x);
display: flex;
gap: var(--cpd-space-4x);
line-height: var(--cpd-space-6x);
&:first-of-type {
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
&:first-of-type {
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
&:last-of-type {
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
&:last-of-type {
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
& > p {
flex: 1;
}
& > p {
flex: 1;
}
& > svg {
display: block;
height: var(--cpd-space-6x);
width: var(--cpd-space-6x);
color: var(--cpd-color-icon-quaternary);
}
}
& > svg {
display: block;
height: var(--cpd-space-6x);
width: var(--cpd-space-6x);
color: var(--cpd-color-icon-quaternary);
}
}
}
}
.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);
}
}

View File

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

View File

@ -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">
{% block content %}{% endblock content %}
<body>
<div class="layout-container">
{% block content %}{% endblock content %}
{% include "components/footer.html" %}
</div>
</body>
</html>

View File

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

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

View File

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

View File

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

View File

@ -17,21 +17,19 @@ 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">
<h1 class="text-xl font-semibold">{{ _("mas.not_found.heading") }}</h1>
<p>{{ _("mas.not_found.description") }}</p>
<div>
{{ button.link_text(text=_("mas.back_to_homepage"), href="/") }}
</div>
<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>
{{ button.link_text(text=_("mas.back_to_homepage"), href="/") }}
</div>
<hr />
<hr />
<code>
<pre class="whitespace-pre-wrap break-all">{{ method }} {{ uri }} {{ version }}
<code>
<pre class="whitespace-pre-wrap break-all">{{ method }} {{ uri }} {{ version }}
{{ version }} 404 Not Found</pre>
</code>
</div>
</section>
</code>
</main>
{% endblock %}

View File

@ -17,23 +17,19 @@ 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 %}
<div class="text-critical font-medium">
{{ errors.form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-critical font-medium">
{{ errors.form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<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>
<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")) }}
</form>
{% endblock content %}

View File

@ -17,25 +17,22 @@ 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.verify_email.headline") }}</h1>
<p>{{ _("mas.verify_email.description", email=email.email) }}</p>
</div>
<form method="POST" class="flex-1 flex flex-col gap-6 justify-center">
<div class="text-center">
<h1 class="cpd-text-heading-xl-semibold">{{ _("mas.verify_email.headline") }}</h1>
<p>{{ _("mas.verify_email.description", email=email.email) }}</p>
</div>
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-critical font-medium">
{{ errors.form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-critical font-medium">
{{ errors.form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ 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>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field.input(label=_("mas.verify_email.code"), name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }}
{{ button.button(text=_("action.submit")) }}
</form>
{% endblock content %}

View File

@ -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>
<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.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") }}
</form>
</section>
<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") }}
{{ 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") }}
</form>
{% endblock content %}

View File

@ -18,59 +18,57 @@ 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">
<div class="flex flex-col gap-2 text-center">
{% if client.logo_uri %}
<img class="consent-client-icon image" referrerpolicy="no-referrer" src="{{ client.logo_uri }}" />
{% else %}
<div class="consent-client-icon generic">
{{ icon.web_browser() }}
</div>
{% endif %}
<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 }}" />
{% else %}
<div class="consent-client-icon generic">
{{ icon.web_browser() }}
</div>
{% endif %}
<h1 class="cpd-text-primary cpd-text-heading-xl-semibold"><a target="_blank" href="{{ client.client_uri }}">{{ client_name }}</a></h1>
<p class="cpd-text-secondary cpd-text-body-lg-regular"><span class="whitespace-nowrap">at {{ grant.redirect_uri | simplify_url }}</span> wants to access your account. This will allow <span class="whitespace-nowrap">{{ client_name }}</span> to:</p>
</div>
<div class="consent-scope-list">
{{ scope.list(scopes=grant.scope) }}
</div>
<div class="my-2 text-center cpd-text-body-md-regular">
<span class="font-semibold">Make sure that you trust <span class="whitespace-nowrap">{{ client_name }}</span>.</span>
You may be sharing sensitive information with this site or app.
{% if client.policy_uri or client.tos_uri %}
Find out how {{ client_name }} will handle your data by reviewing its
{% if client.policy_uri %}
<a target="_blank" href="{{ client.policy_uri }}" class="cpd-link" data-kind="primary">privacy policy</a>{% if not client.tos_uri %}.{% endif %}
{% endif %}
{% if client.policy_uri and client.tos_uri%}
and
{% endif %}
{% if client.tos_uri %}
<a target="_blank" href="{{ client.tos_uri }}" class="cpd-link" data-kind="primary">terms of service</a>.
{% endif %}
{% endif %}
</div>
<form method="POST" class="flex flex-col">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ button.button(text=_("action.continue")) }}
</form>
{{ back_to_client.link(
text=_("action.cancel"),
kind="tertiary",
uri=grant.redirect_uri,
mode=grant.response_mode,
params=dict(error="access_denied", state=grant.state)
) }}
<div class="text-center">
{{ _("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>
<h1 class="cpd-text-primary cpd-text-heading-xl-semibold"><a target="_blank" href="{{ client.client_uri }}">{{ client_name }}</a></h1>
<p class="cpd-text-secondary cpd-text-body-lg-regular"><span class="whitespace-nowrap">at {{ grant.redirect_uri | simplify_url }}</span> wants to access your account. This will allow <span class="whitespace-nowrap">{{ client_name }}</span> to:</p>
</div>
</section>
<div class="consent-scope-list">
{{ scope.list(scopes=grant.scope) }}
</div>
<div class="my-2 text-center cpd-text-body-md-regular">
<span class="font-semibold">Make sure that you trust <span class="whitespace-nowrap">{{ client_name }}</span>.</span>
You may be sharing sensitive information with this site or app.
{% if client.policy_uri or client.tos_uri %}
Find out how {{ client_name }} will handle your data by reviewing its
{% if client.policy_uri %}
<a target="_blank" href="{{ client.policy_uri }}" class="cpd-link" data-kind="primary">privacy policy</a>{% if not client.tos_uri %}.{% endif %}
{% endif %}
{% if client.policy_uri and client.tos_uri%}
and
{% endif %}
{% if client.tos_uri %}
<a target="_blank" href="{{ client.tos_uri }}" class="cpd-link" data-kind="primary">terms of service</a>.
{% endif %}
{% endif %}
</div>
<form method="POST" class="flex flex-col">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ button.button(text=_("action.continue")) }}
</form>
{{ back_to_client.link(
text=_("action.cancel"),
kind="tertiary",
uri=grant.redirect_uri,
mode=grant.response_mode,
params=dict(error="access_denied", state=grant.state)
) }}
<div class="text-center">
{{ _("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>
</main>
{% endblock content %}

View File

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

View File

@ -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">
{{ _("app.technical_description", discovery_url=discovery_url) }}
<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>
{% if current_session %}
<p class="cpd-text-body-md-regular">
{{ _("mas.navbar.signed_in_as", username=current_session.user.username) }}
</p>
</div>
</section>
{{ 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 %}

View File

@ -17,21 +17,21 @@ 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">
{% if not password_disabled %}
{% if next and next.kind == "link_upstream" %}
<div class="text-center">
<h1 class="text-lg text-center font-medium">{{ _("mas.login.link.headline") }}</h1>
<p class="text-sm">{{ _("mas.login.link.description", provider=next.provider.issuer) }}</p>
</div>
{% else %}
<div class="text-center">
<h1 class="text-lg text-center font-medium">{{ _("mas.login.headline") }}</h1>
<p>{{ _("mas.login.description") }}</p>
</div>
{% endif %}
<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">
<h1 class="text-lg text-center font-medium">{{ _("mas.login.link.headline") }}</h1>
<p class="text-sm">{{ _("mas.login.link.description", provider=next.provider.issuer) }}</p>
</div>
{% else %}
<div class="text-center">
<h1 class="text-lg text-center font-medium">{{ _("mas.login.headline") }}</h1>
<p>{{ _("mas.login.description") }}</p>
</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,52 +43,43 @@ 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 %}
{{ button.button(text=_("action.continue")) }}
</form>
{% if not next or next.kind != "link_upstream" %}
<div class="text-center mt-4">
{{ _("mas.login.call_to_register") }}
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
{{ button.link_text(text=_("action.create_account"), href="/register" ~ params) }}
</div>
{% endif %}
{% endif %}
{% 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>
{% endif %}
{% for provider in providers %}
{% if not next or next.kind != "link_upstream" %}
<div class="text-center mt-4">
{{ _("mas.login.call_to_register") }}
{% 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) }}
{% endfor %}
{% endif %}
{% if not providers and password_disabled %}
<div class="text-center">
{{ _("mas.login.no_login_methods") }}
{{ button.link_text(text=_("action.create_account"), href="/register" ~ params) }}
</div>
{% endif %}
</form>
</section>
{% endif %}
{% if providers %}
{% if not password_disabled %}
{{ field.separator() }}
{% endif %}
{% for provider in providers %}
{% set params = next["params"] | default({}) | to_params(prefix="?") %}
<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 %}
{% if not providers and password_disabled %}
<div class="text-center">
{{ _("mas.login.no_login_methods") }}
</div>
{% endif %}
{% 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 %}

View File

@ -17,37 +17,33 @@ 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>
<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">
{% 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>
</div>
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex items-center">
<div class="text-center flex-1">
{{ _("mas.policy_violation.logged_as", username=current_session.user.username) }}
</div>
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action) }}
</div>
{{ back_to_client.link(
text=_("action.cancel"),
kind="destructive",
uri=grant.redirect_uri,
mode=grant.response_mode,
params=dict(error="access_denied", state=grant.state)
) }}
<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="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>
<a target="_blank" href="{{ client.client_uri }}" class="cpd-link" data-kind="primary">{{ client.client_name | default(client.client_id) }}</a>
</div>
<div class="flex gap-4 justify-center items-center">
<p>
{{ _("mas.policy_violation.logged_as", username=current_session.user.username) }}
</p>
{{ logout.button(text=_("action.sign_out"), csrf_token=csrf_token, post_logout_action=action, as_link=True) }}
</div>
{{ back_to_client.link(
text=_("action.cancel"),
kind="destructive",
uri=grant.redirect_uri,
mode=grant.response_mode,
params=dict(error="access_denied", state=grant.state)
) }}
</section>
{% endblock content %}

View File

@ -17,38 +17,33 @@ 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">
<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>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{# TODO: errors #}
{{ 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="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>
<div class="text-center mt-4">
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>
<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" %}
{{ back_to_client.link(
text="Cancel",
kind="destructive",
uri=next.grant.redirect_uri,
mode=next.grant.response_mode,
params=dict(error="access_denied", state=next.grant.state)
) }}
{% endif %}
<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>
</section>
{% endblock content %}

View File

@ -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">
<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>
<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,29 +37,23 @@ 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") }}
{% 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 %}
<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>
{{ button.button(text=_("action.continue")) }}
</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 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>
</section>
{% endblock content %}

View File

@ -18,34 +18,32 @@ 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">
<div class="flex flex-col gap-2 text-center">
<div class="consent-client-icon generic">
{{ icon.web_browser() }}
</div>
<p class="cpd-text-secondary cpd-text-body-lg-regular"><span class="whitespace-nowrap">{{ client_name }}</span> wants to access your account. This will allow <span class="whitespace-nowrap">{{ client_name }}</span> to:</p>
<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() }}
</div>
<div class="consent-scope-list">
{{ scope.list(scopes="openid urn:matrix:org.matrix.msc2967.client:api:*") }}
</div>
<div class="my-2 text-center cpd-text-body-md-regular">
<span class="font-semibold">Make sure that you trust <span class="whitespace-nowrap">{{ client_name }}</span>.</span>
You may be sharing sensitive information with this site or app.
</div>
<form method="POST" class="flex flex-col">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ button.button(text=_("action.continue")) }}
</form>
<div class="text-center">
{{ _("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>
<p class="cpd-text-secondary cpd-text-body-lg-regular"><span class="whitespace-nowrap">{{ client_name }}</span> wants to access your account. This will allow <span class="whitespace-nowrap">{{ client_name }}</span> to:</p>
</div>
</section>
<div class="consent-scope-list">
{{ scope.list(scopes="openid urn:matrix:org.matrix.msc2967.client:api:*") }}
</div>
<div class="my-2 text-center cpd-text-body-md-regular">
<span class="font-semibold">Make sure that you trust <span class="whitespace-nowrap">{{ client_name }}</span>.</span>
You may be sharing sensitive information with this site or app.
</div>
<form method="POST" class="flex flex-col">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ button.button(text=_("action.continue")) }}
</form>
<div class="text-center">
{{ _("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>
</main>
{% endblock content %}

View File

@ -17,64 +17,58 @@ 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">
{% if force_localpart %}
{{ _("mas.upstream_oauth2.register.create_account") }}
{% else %}
{{ _("mas.upstream_oauth2.register.choose_username") }}
{% endif %}
</h1>
<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 %}
{{ _("mas.upstream_oauth2.register.choose_username") }}
{% endif %}
</h1>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<input type="hidden" name="action" value="register" />
{% if force_localpart %}
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-4">
<div class="font-medium"> {{ _("mas.upstream_oauth2.register.forced_localpart") }}</div>
<div class="font-mono">{{ suggested_localpart }}</div>
<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 %}
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-4">
<div class="font-medium"> {{ _("mas.upstream_oauth2.register.forced_localpart") }}</div>
<div class="font-mono">{{ suggested_localpart }}</div>
</div>
{% else %}
{{ field.input(label=_("common.username"), name="username", autocomplete="username", autocorrect="off", autocapitalize="none") }}
{% endif %}
{% if suggested_email %}
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-4">
<div class="font-medium">
{% if force_email %}
{{ _("mas.upstream_oauth2.register.forced_email") }}
{% else %}
<input type="checkbox" name="import_email" id="import_email" checked="checked" />
<label for="import_email">{{ _("mas.upstream_oauth2.register.suggested_email") }}</label>
{% endif %}
</div>
{% else %}
{{ field.input(label=_("common.username"), name="username", autocomplete="username", autocorrect="off", autocapitalize="none") }}
{% endif %}
<div class="font-mono">{{ suggested_email }}</div>
</div>
{% endif %}
{% if suggested_email %}
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-4">
<div class="font-medium">
{% if force_email %}
{{ _("mas.upstream_oauth2.register.forced_email") }}
{% else %}
<input type="checkbox" name="import_email" id="import_email" checked="checked" />
<label for="import_email">{{ _("mas.upstream_oauth2.register.suggested_email") }}</label>
{% endif %}
</div>
<div class="font-mono">{{ suggested_email }}</div>
{% if suggested_display_name %}
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-4">
<div class="font-medium">
{% if force_display_name %}
{{ _("mas.upstream_oauth2.register.forced_display_name") }}
{% else %}
<input type="checkbox" name="import_display_name" id="import_display_name" checked="checked" />
<label for="import_display_name">{{ _("mas.upstream_oauth2.register.suggested_display_name") }}</label>
{% endif %}
</div>
{% endif %}
<div class="font-mono">{{ suggested_display_name }}</div>
</div>
{% endif %}
{% if suggested_display_name %}
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-4">
<div class="font-medium">
{% if force_display_name %}
{{ _("mas.upstream_oauth2.register.forced_display_name") }}
{% else %}
<input type="checkbox" name="import_display_name" id="import_display_name" checked="checked" />
<label for="import_display_name">{{ _("mas.upstream_oauth2.register.suggested_display_name") }}</label>
{% endif %}
</div>
<div class="font-mono">{{ suggested_display_name }}</div>
</div>
{% endif %}
{{ 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>
{{ button.link_outline(text=_("mas.upstream_oauth2.register.link_existing"), href=login_link) }}
</div>
{{ button.button(text=_("action.create_account")) }}
</form>
{{ field.separator() }}
{{ button.link_outline(text=_("mas.upstream_oauth2.register.link_existing"), href=login_link) }}
</section>
{% endblock content %}

View File

@ -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">
{{ _("mas.upstream_oauth2.link_mismatch.heading") }}
</h1>
<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 %}

View File

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

View File

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