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

Do not embed the templates and static files in the binary

This commit is contained in:
Quentin Gliech
2022-11-18 21:03:04 +01:00
parent 834214bcac
commit 9c0ece7512
51 changed files with 150 additions and 3518 deletions

View File

@ -49,7 +49,6 @@ mas-listener = { path = "../listener" }
mas-policy = { path = "../policy" }
mas-router = { path = "../router" }
mas-spa = { path = "../spa" }
mas-static-files = { path = "../static-files" }
mas-storage = { path = "../storage" }
mas-tasks = { path = "../tasks" }
mas-templates = { path = "../templates" }
@ -71,9 +70,6 @@ native-roots = ["mas-http/native-roots", "mas-handlers/native-roots"]
# Use the webpki root certificates
webpki-roots = ["mas-http/webpki-roots", "mas-handlers/webpki-roots"]
# Read the builtin static files and templates from the source directory
dev = ["mas-templates/dev", "mas-static-files/dev"]
# Enable OpenTelemetry OTLP exporter.
otlp = ["dep:opentelemetry-otlp"]
# Enable OpenTelemetry Jaeger exporter and propagator.

View File

@ -55,44 +55,36 @@ async fn watch_templates(
let templates = templates.clone();
// Find which roots we're supposed to watch
let roots = templates.watch_roots().await;
let mut streams = Vec::new();
// Find which root we're supposed to watch
let root = templates.watch_root();
for root in roots {
// For each root, create a subscription
let resolved = client
.resolve_root(CanonicalPath::canonicalize(root)?)
.await?;
// For each root, create a subscription
let resolved = client
.resolve_root(CanonicalPath::canonicalize(root)?)
.await?;
// TODO: we could subscribe to less, properly filter here
let (subscription, _) = client
.subscribe::<NameOnly>(&resolved, SubscribeRequest::default())
.await?;
// TODO: we could subscribe to less, properly filter here
let (subscription, _) = client
.subscribe::<NameOnly>(&resolved, SubscribeRequest::default())
.await?;
// Create a stream out of that subscription
let stream = futures_util::stream::try_unfold(subscription, |mut sub| async move {
let next = sub.next().await?;
anyhow::Ok(Some((next, sub)))
});
streams.push(Box::pin(stream));
}
let files_changed_stream =
futures_util::stream::select_all(streams).try_filter_map(|event| async move {
match event {
SubscriptionData::FilesChanged(QueryResult {
files: Some(files), ..
}) => {
let files: Vec<_> = files.into_iter().map(|f| f.name.into_inner()).collect();
Ok(Some(files))
}
_ => Ok(None),
// Create a stream out of that subscription
let fut = futures_util::stream::try_unfold(subscription, |mut sub| async move {
let next = sub.next().await?;
anyhow::Ok(Some((next, sub)))
})
.try_filter_map(|event| async move {
match event {
SubscriptionData::FilesChanged(QueryResult {
files: Some(files), ..
}) => {
let files: Vec<_> = files.into_iter().map(|f| f.name.into_inner()).collect();
Ok(Some(files))
}
});
let fut = files_changed_stream.for_each(move |files| {
_ => Ok(None),
}
})
.for_each(move |files| {
let templates = templates.clone();
async move {
info!(?files, "Files changed, reloading templates");
@ -162,13 +154,9 @@ impl Options {
let url_builder = UrlBuilder::new(config.http.public_base.clone());
// Load and compile the templates
let templates = Templates::load(
config.templates.path.clone(),
config.templates.builtin,
url_builder.clone(),
)
.await
.context("could not load templates")?;
let templates = Templates::load(config.templates.path.clone(), url_builder.clone())
.await
.context("could not load templates")?;
let mailer = Mailer::new(
&templates,

View File

@ -25,24 +25,10 @@ pub(super) struct Options {
#[derive(Parser, Debug)]
enum Subcommand {
/// Save the builtin templates to a folder
Save {
/// Where the templates should be saved
path: Utf8PathBuf,
/// Overwrite existing template files
#[arg(long)]
overwrite: bool,
},
/// Check for template validity at given path.
Check {
/// Path where the templates are
path: String,
/// Skip loading builtin templates
#[arg(long)]
skip_builtin: bool,
path: Utf8PathBuf,
},
}
@ -50,17 +36,10 @@ impl Options {
pub async fn run(&self, _root: &super::Options) -> anyhow::Result<()> {
use Subcommand as SC;
match &self.subcommand {
SC::Save { path, overwrite } => {
Templates::save(path, *overwrite).await?;
Ok(())
}
SC::Check { path, skip_builtin } => {
SC::Check { path } => {
let clock = Clock::default();
let url_builder = mas_router::UrlBuilder::new("https://example.com/".parse()?);
let templates =
Templates::load(Some(path.into()), !skip_builtin, url_builder).await?;
let templates = Templates::load(path.clone(), url_builder).await?;
templates.check_render(clock.now()).await?;
Ok(())

View File

@ -59,9 +59,15 @@ where
mas_config::HttpResource::GraphQL { playground } => {
router.merge(mas_handlers::graphql_router::<AppState, B>(*playground))
}
mas_config::HttpResource::Static { web_root } => {
let handler = mas_static_files::service(web_root.as_deref());
router.nest_service(mas_router::StaticAsset::route(), handler)
mas_config::HttpResource::Assets { path } => {
let static_service = ServeDir::new(path).append_index_html_on_directories(false);
let error_layer =
HandleErrorLayer::new(|_e| ready(StatusCode::INTERNAL_SERVER_ERROR));
router.nest_service(
mas_router::StaticAsset::route(),
error_layer.layer(static_service),
)
}
mas_config::HttpResource::OAuth => {
router.merge(mas_handlers::api_router::<AppState, B>())
@ -77,13 +83,11 @@ where
}),
),
mas_config::HttpResource::Spa { assets, manifest } => {
mas_config::HttpResource::Spa { manifest } => {
let error_layer =
HandleErrorLayer::new(|_e| ready(StatusCode::INTERNAL_SERVER_ERROR));
// TODO: split the assets service and the index service, and make those paths
// configurable
let assets_base = "/app-assets/";
// TODO: make those paths configurable
let app_base = "/app/";
// TODO: make that config typed and configurable
@ -91,14 +95,13 @@ where
"root": app_base,
});
let index_service =
ViteManifestService::new(manifest.clone(), assets_base.into(), config);
let index_service = ViteManifestService::new(
manifest.clone(),
mas_router::StaticAsset::route().into(),
config,
);
let static_service = ServeDir::new(assets).append_index_html_on_directories(false);
router
.nest_service(app_base, error_layer.layer(index_service))
.nest_service(assets_base, error_layer.layer(static_service))
router.nest_service(app_base, error_layer.layer(index_service))
}
}
}

View File

@ -49,18 +49,18 @@ fn http_listener_spa_manifest_default() -> Utf8PathBuf {
}
#[cfg(not(feature = "docker"))]
fn http_listener_spa_assets_default() -> Utf8PathBuf {
fn http_listener_assets_path_default() -> Utf8PathBuf {
"./frontend/dist/".into()
}
#[cfg(feature = "docker")]
fn http_listener_spa_manifest_default() -> Utf8PathBuf {
"/usr/local/share/mas-cli/frontend-manifest.json".into()
"/usr/local/share/mas-cli/manifest.json".into()
}
#[cfg(feature = "docker")]
fn http_listener_spa_assets_default() -> Utf8PathBuf {
"/usr/local/share/mas-cli/frontend-assets/".into()
fn http_listener_assets_path_default() -> Utf8PathBuf {
"/usr/local/share/mas-cli/assets/".into()
}
/// Kind of socket
@ -272,12 +272,11 @@ pub enum Resource {
Compat,
/// Static files
Static {
/// Path from which to serve static files. If not specified, it will
/// serve the static files embedded in the server binary
#[serde(default)]
#[schemars(with = "Option<String>")]
web_root: Option<Utf8PathBuf>,
Assets {
/// Path to the directory to serve.
#[serde(default = "http_listener_assets_path_default")]
#[schemars(with = "String")]
path: Utf8PathBuf,
},
/// Mount a "/connection-info" handler which helps debugging informations on
@ -287,15 +286,10 @@ pub enum Resource {
/// Mount the single page app
Spa {
/// Path to the vite mamanifest.jsonnifest
/// Path to the vite manifest.json
#[serde(default = "http_listener_spa_manifest_default")]
#[schemars(with = "String")]
manifest: Utf8PathBuf,
/// Path to the assets to server
#[serde(default = "http_listener_spa_assets_default")]
#[schemars(with = "String")]
assets: Utf8PathBuf,
},
}
@ -346,10 +340,11 @@ impl Default for HttpConfig {
Resource::OAuth,
Resource::Compat,
Resource::GraphQL { playground: true },
Resource::Static { web_root: None },
Resource::Assets {
path: http_listener_assets_path_default(),
},
Resource::Spa {
manifest: http_listener_spa_manifest_default(),
assets: http_listener_spa_assets_default(),
},
],
tls: None,

View File

@ -20,28 +20,29 @@ use serde::{Deserialize, Serialize};
use super::ConfigurationSection;
fn default_builtin() -> bool {
true
#[cfg(not(feature = "docker"))]
fn default_path() -> Utf8PathBuf {
"./templates/".into()
}
#[cfg(feature = "docker")]
fn default_path() -> Utf8PathBuf {
"/usr/local/share/mas-cli/templates/".into()
}
/// Configuration related to templates
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
pub struct TemplatesConfig {
/// Path to the folder that holds the custom templates
#[serde(default)]
/// Path to the folder which holds the templates
#[serde(default = "default_path")]
#[schemars(with = "Option<String>")]
pub path: Option<Utf8PathBuf>,
/// Load the templates embedded in the binary
#[serde(default = "default_builtin")]
pub builtin: bool,
pub path: Utf8PathBuf,
}
impl Default for TemplatesConfig {
fn default() -> Self {
Self {
path: None,
builtin: default_builtin(),
path: default_path(),
}
}
}

View File

@ -43,6 +43,7 @@ serde_urlencoded = "0.7.1"
argon2 = { version = "0.4.1", features = ["password-hash"] }
# Various data types and utilities
camino = "1.1.1"
chrono = { version = "0.4.23", features = ["serde"] }
url = { version = "2.3.1", features = ["serde"] }
mime = "0.3.16"

View File

@ -362,9 +362,13 @@ where
async fn test_state(pool: PgPool) -> Result<AppState, anyhow::Error> {
use mas_email::MailTransport;
let workspace_root = camino::Utf8Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..");
let url_builder = UrlBuilder::new("https://example.com/".parse()?);
let templates = Templates::load(None, true, url_builder.clone()).await?;
let templates = Templates::load(workspace_root.join("templates"), url_builder.clone()).await?;
// TODO: add test keys to the store
let key_store = Keystore::default();
@ -377,14 +381,7 @@ async fn test_state(pool: PgPool) -> Result<AppState, anyhow::Error> {
let homeserver = MatrixHomeserver::new("example.com".to_owned());
#[allow(clippy::disallowed_types)]
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("policies")
.join("policy.wasm");
let file = tokio::fs::File::open(path).await?;
let file = tokio::fs::File::open(workspace_root.join("policies").join("policy.wasm")).await?;
let policy_factory = PolicyFactory::load(
file,

View File

@ -411,8 +411,7 @@ mod tests {
fn timestamp_serde() {
let datetime = Timestamp(
chrono::Utc
.ymd_opt(2018, 1, 18)
.and_hms_opt(1, 30, 22)
.with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
.unwrap(),
);
let timestamp = serde_json::Value::Number(1_516_239_022.into());
@ -451,8 +450,7 @@ mod tests {
#[test]
fn extract_claims() {
let now = chrono::Utc
.ymd_opt(2018, 1, 18)
.and_hms_opt(1, 30, 22)
.with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
.unwrap();
let expiration = now + chrono::Duration::minutes(5);
let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
@ -496,8 +494,7 @@ mod tests {
#[test]
fn time_validation() {
let now = chrono::Utc
.ymd_opt(2018, 1, 18)
.and_hms_opt(1, 30, 22)
.with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
.unwrap();
let claims = serde_json::json!({
@ -604,8 +601,7 @@ mod tests {
#[test]
fn invalid_claims() {
let now = chrono::Utc
.ymd_opt(2018, 1, 18)
.and_hms_opt(1, 30, 22)
.with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
.unwrap();
let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());

View File

@ -539,7 +539,7 @@ impl StaticAsset {
impl Route for StaticAsset {
type Query = ();
fn route() -> &'static str {
"/assets"
"/assets/"
}
fn path(&self) -> std::borrow::Cow<'static, str> {

View File

@ -1,2 +0,0 @@
/node_modules/
/public/tailwind.css

View File

@ -1,21 +0,0 @@
[package]
name = "mas-static-files"
version = "0.1.0"
authors = ["Quentin Gliech <quenting@element.io>"]
edition = "2021"
license = "Apache-2.0"
[features]
dev = []
[dependencies]
axum = { version = "0.6.0-rc.4", features = ["headers"] }
camino = "1.1.1"
headers = "0.3.8"
http = "0.2.8"
http-body = "0.4.5"
mime_guess = "2.0.4"
rust-embed = "6.4.2"
tower = "0.4.13"
tower-http = { version = "0.3.4", features = ["fs"] }
tracing = "0.1.37"

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +0,0 @@
{
"name": "static-files",
"private": true,
"scripts": {
"build": "tailwindcss --postcss -o public/tailwind.css",
"start": "tailwindcss --postcss -o public/tailwind.css --watch"
},
"license": "Apache-2.0",
"devDependencies": {
"@tailwindcss/forms": "^0.5.3",
"autoprefixer": "^10.4.13",
"cssnano": "^5.1.14",
"postcss": "^8.4.19",
"tailwindcss": "^3.2.4"
}
}

View File

@ -1,21 +0,0 @@
// Copyright 2021 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.
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
cssnano: {},
}
}

View File

@ -1 +0,0 @@
ok

View File

@ -1,171 +0,0 @@
// Copyright 2021, 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.
//! Serve static files used by the web frontend
#![forbid(unsafe_code)]
#![deny(
clippy::all,
clippy::str_to_string,
missing_docs,
rustdoc::broken_intra_doc_links
)]
#![warn(clippy::pedantic)]
#[cfg(not(feature = "dev"))]
mod builtin {
// the RustEmbed derive uses std::path::Path
#![allow(clippy::disallowed_types)]
use std::{
fmt::Write,
future::{ready, Ready},
};
use axum::{
response::{IntoResponse, Response},
TypedHeader,
};
use headers::{ContentLength, ContentType, ETag, HeaderMapExt, IfNoneMatch};
use http::{Method, Request, StatusCode};
use rust_embed::RustEmbed;
use tower::Service;
/// Embedded public assets
#[derive(RustEmbed, Clone, Debug)]
#[folder = "public/"]
pub struct Assets;
impl Assets {
fn get_response(
is_head: bool,
path: &str,
if_none_match: Option<IfNoneMatch>,
) -> Option<Response> {
let asset = Self::get(path)?;
let etag = {
let hash = asset.metadata.sha256_hash();
let mut buf = String::with_capacity(2 + hash.len() * 2);
write!(buf, "\"").unwrap();
for byte in hash {
write!(buf, "{:02x}", byte).unwrap();
}
write!(buf, "\"").unwrap();
buf
};
let etag: ETag = etag.parse().unwrap();
if let Some(if_none_match) = if_none_match {
if if_none_match.precondition_passes(&etag) {
return Some(StatusCode::NOT_MODIFIED.into_response());
}
}
let len = asset.data.len().try_into().unwrap();
let mime = mime_guess::from_path(path).first_or_octet_stream();
let headers = (
TypedHeader(ContentType::from(mime)),
TypedHeader(ContentLength(len)),
TypedHeader(etag),
);
let res = if is_head {
headers.into_response()
} else {
(headers, asset.data).into_response()
};
Some(res)
}
}
impl<B> Service<Request<B>> for Assets {
type Response = Response;
type Error = std::io::Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(
&mut self,
_cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<B>) -> Self::Future {
let (parts, _body) = req.into_parts();
let path = parts.uri.path().trim_start_matches('/');
let if_none_match = parts.headers.typed_get();
let is_head = match parts.method {
Method::GET => false,
Method::HEAD => true,
_ => return ready(Ok(StatusCode::METHOD_NOT_ALLOWED.into_response())),
};
// TODO: support range requests
let response = Self::get_response(is_head, path, if_none_match)
.unwrap_or_else(|| StatusCode::NOT_FOUND.into_response());
ready(Ok(response))
}
}
/// Serve static files
#[must_use]
pub fn service() -> Assets {
Assets
}
}
#[cfg(feature = "dev")]
mod builtin {
use camnio::Utf8Path;
use tower_http::services::ServeDir;
/// Serve static files in dev mode
#[must_use]
pub fn service() -> ServeDir {
let path = Utf8Path::new(format!("{}/public", env!("CARGO_MANIFEST_DIR")));
ServeDir::new(path).append_index_html_on_directories(false)
}
}
use std::{convert::Infallible, future::ready};
use axum::{
body::HttpBody,
response::Response,
routing::{on_service, MethodFilter},
};
use camino::Utf8Path;
use http::{Request, StatusCode};
use tower::{util::BoxCloneService, ServiceExt};
use tower_http::services::ServeDir;
/// Serve static files
#[must_use]
pub fn service<B: HttpBody + Send + 'static>(
path: Option<&Utf8Path>,
) -> BoxCloneService<Request<B>, Response, Infallible> {
let svc = if let Some(path) = path {
let handler = ServeDir::new(path).append_index_html_on_directories(false);
on_service(MethodFilter::HEAD | MethodFilter::GET, handler)
} else {
let builtin = self::builtin::service();
on_service(MethodFilter::HEAD | MethodFilter::GET, builtin)
};
svc.handle_error(|_| ready(StatusCode::INTERNAL_SERVER_ERROR))
.boxed_clone()
}

View File

@ -1,46 +0,0 @@
// Copyright 2021 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.
module.exports = {
mode: "jit",
content: ["../templates/src/res/**/*.html"],
theme: {
extend: {
colors: {
accent: '#0DBD8B',
alert: '#FF5B55',
links: '#0086E6',
'grey-25': '#F4F6FA',
'grey-50': '#E3E8F0',
'grey-100': '#C1C6CD',
'grey-150': '#8D97A5',
'grey-200': '#737D8C',
'grey-250': '#A9B2BC',
'grey-300': '#8E99A4',
'grey-400': '#6F7882',
'grey-450': '#394049',
'black-800': '#15191E',
'black-900': '#17191C',
'black-950': '#21262C',
'ice': '#F4F9FD',
},
},
},
variants: {
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
],
}

View File

@ -5,9 +5,6 @@ authors = ["Quentin Gliech <quenting@element.io>"]
edition = "2021"
license = "Apache-2.0"
[features]
dev = []
[dependencies]
tracing = "0.1.37"
tokio = { version = "1.21.2", features = ["macros"] }

View File

@ -24,16 +24,16 @@
//! Templates rendering
use std::{collections::HashSet, io::Cursor, string::ToString, sync::Arc};
use std::{collections::HashSet, string::ToString, sync::Arc};
use anyhow::{bail, Context as _};
use anyhow::Context as _;
use camino::{Utf8Path, Utf8PathBuf};
use mas_data_model::StorageBackend;
use mas_router::UrlBuilder;
use serde::Serialize;
use tera::{Context, Error as TeraError, Tera};
use thiserror::Error;
use tokio::{fs::OpenOptions, io::AsyncWriteExt, sync::RwLock, task::JoinError};
use tokio::{sync::RwLock, task::JoinError};
use tracing::{debug, info, warn};
mod context;
@ -59,13 +59,16 @@ pub use self::{
pub struct Templates {
tera: Arc<RwLock<Tera>>,
url_builder: UrlBuilder,
path: Option<Utf8PathBuf>,
builtin: bool,
path: Utf8PathBuf,
}
/// There was an issue while loading the templates
#[derive(Error, Debug)]
pub enum TemplateLoadingError {
/// I/O error
#[error(transparent)]
IO(#[from] std::io::Error),
/// Some templates failed to compile
#[error("could not load and compile some templates")]
Compile(#[from] TeraError),
@ -85,116 +88,42 @@ pub enum TemplateLoadingError {
}
impl Templates {
/// List directories to watch
pub async fn watch_roots(&self) -> Vec<Utf8PathBuf> {
Self::roots(self.path.as_deref(), self.builtin)
.await
.into_iter()
.filter_map(Result::ok)
.collect()
}
async fn roots(
path: Option<&Utf8Path>,
builtin: bool,
) -> Vec<Result<Utf8PathBuf, std::io::Error>> {
let mut paths = Vec::new();
if builtin && cfg!(feature = "dev") {
paths.push(
Utf8Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("res"),
);
}
if let Some(path) = path {
paths.push(Utf8PathBuf::from(path));
}
let mut ret = Vec::new();
for path in paths {
ret.push(tokio::fs::read_dir(&path).await.map(|_| path));
}
ret
}
fn load_builtin() -> Result<Tera, TemplateLoadingError> {
let mut tera = Tera::default();
info!("Loading builtin templates");
tera.add_raw_templates(
EXTRA_TEMPLATES
.into_iter()
.chain(TEMPLATES)
.filter_map(|(name, content)| content.map(|c| (name, c))),
)?;
Ok(tera)
/// Directories to watch
#[must_use]
pub fn watch_root(&self) -> &Utf8Path {
&self.path
}
/// Load the templates from the given config
pub async fn load(
path: Option<Utf8PathBuf>,
builtin: bool,
path: Utf8PathBuf,
url_builder: UrlBuilder,
) -> Result<Self, TemplateLoadingError> {
let tera = Self::load_(path.as_deref(), builtin, url_builder.clone()).await?;
let tera = Self::load_(&path, url_builder.clone()).await?;
Ok(Self {
tera: Arc::new(RwLock::new(tera)),
path,
url_builder,
builtin,
})
}
async fn load_(
path: Option<&Utf8Path>,
builtin: bool,
url_builder: UrlBuilder,
) -> Result<Tera, TemplateLoadingError> {
let mut teras = Vec::new();
async fn load_(path: &Utf8Path, url_builder: UrlBuilder) -> Result<Tera, TemplateLoadingError> {
let path = path.to_owned();
let roots = Self::roots(path, builtin).await;
for maybe_root in roots {
let root = match maybe_root {
Ok(root) => root,
Err(err) => {
warn!(%err, "Could not open a template folder, skipping it");
continue;
}
};
// This uses blocking I/Os, do that in a blocking task
let mut tera = tokio::task::spawn_blocking(move || {
let path = path.canonicalize_utf8()?;
let path = format!("{}/**/*.{{html,txt,subject}}", path);
// This uses blocking I/Os, do that in a blocking task
let tera = tokio::task::spawn_blocking(move || {
let path = format!("{}/**/*.{{html,txt,subject}}", root);
info!(%path, "Loading templates from filesystem");
Tera::parse(&path)
})
.await??;
teras.push(tera);
}
if builtin {
teras.push(Self::load_builtin()?);
}
// Merging all Tera instances into a single one
let mut tera = teras
.into_iter()
.try_fold(Tera::default(), |mut acc, tera| {
acc.extend(&tera)?;
Ok::<_, TemplateLoadingError>(acc)
})?;
tera.build_inheritance_chains()?;
tera.check_macro_files()?;
info!(%path, "Loading templates from filesystem");
Tera::new(&path)
})
.await??;
self::functions::register(&mut tera, url_builder);
let loaded: HashSet<_> = tera.get_template_names().collect();
let needed: HashSet<_> = TEMPLATES.into_iter().map(|(name, _)| name).collect();
let needed: HashSet<_> = TEMPLATES.into_iter().collect();
debug!(?loaded, ?needed, "Templates loaded");
let missing: HashSet<_> = needed.difference(&loaded).collect();
@ -210,61 +139,13 @@ impl Templates {
/// Reload the templates on disk
pub async fn reload(&self) -> anyhow::Result<()> {
// Prepare the new Tera instance
let new_tera =
Self::load_(self.path.as_deref(), self.builtin, self.url_builder.clone()).await?;
let new_tera = Self::load_(&self.path, self.url_builder.clone()).await?;
// Swap it
*self.tera.write().await = new_tera;
Ok(())
}
/// Save the builtin templates to a folder
pub async fn save(path: &Utf8Path, overwrite: bool) -> anyhow::Result<()> {
if cfg!(feature = "dev") {
bail!("Builtin templates are not included in dev binaries")
}
let templates = TEMPLATES.into_iter().chain(EXTRA_TEMPLATES);
let mut options = OpenOptions::new();
if overwrite {
options.create(true).truncate(true).write(true);
} else {
// With the `create_new` flag, `open` fails with an `AlreadyExists` error to
// avoid overwriting
options.create_new(true).write(true);
};
for (name, source) in templates {
if let Some(source) = source {
let path = path.join(name);
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(&parent)
.await
.context("could not create destination")?;
}
let mut file = match options.open(&path).await {
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
// Not overwriting a template is a soft error
warn!(?path, "Not overwriting template");
continue;
}
x => x.context(format!("could not open file {:?}", path))?,
};
let mut buffer = Cursor::new(source);
file.write_all_buf(&mut buffer)
.await
.context(format!("could not write file {:?}", path))?;
info!(?path, "Wrote template");
}
}
Ok(())
}
}
/// Failed to render a template
@ -294,16 +175,6 @@ pub enum TemplateError {
}
register_templates! {
extra = {
"components/button.html",
"components/field.html",
"components/back_to_client.html",
"components/logout.html",
"components/navbar.html",
"components/errors.html",
"base.html",
};
/// Render the login page
pub fn render_login(WithCsrf<LoginContext>) { "pages/login.html" }
@ -390,8 +261,9 @@ mod tests {
#[allow(clippy::disallowed_methods)]
let now = chrono::Utc::now();
let path = Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../templates/");
let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap());
let templates = Templates::load(None, true, url_builder).await.unwrap();
let templates = Templates::load(path, url_builder).await.unwrap();
templates.check_render(now).await.unwrap();
}
}

View File

@ -49,28 +49,7 @@ macro_rules! register_templates {
)*
} => {
/// List of registered templates
static TEMPLATES: [(&'static str, Option<&'static str>); count!( $( $template )* )] = [
$( (
$template,
if cfg!(feature = "dev") {
None
} else {
Some(include_str!(concat!("res/", $template)))
}
) ),*
];
/// List of extra templates used by other templates
static EXTRA_TEMPLATES: [(&'static str, Option<&'static str>); count!( $( $( $extra_template )* )? )] = [
$( $( (
$extra_template,
if cfg!(feature = "dev") {
None
} else {
Some(include_str!(concat!("res/", $extra_template)))
}
) ),* )?
];
static TEMPLATES: [&'static str; count!( $( $template )* )] = [ $( $template, )* ];
impl Templates {
$(

View File

@ -1,35 +0,0 @@
{#
Copyright 2021, 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.
#}
{% import "components/button.html" as button %}
{% 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 %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{% block title %}matrix-authentication-service{% endblock title %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="{{ static_asset(path='tailwind.css') }}">
</head>
<body class="bg-white text-black-900 dark:bg-black-800 dark:text-white flex flex-col min-h-screen">
{% block content %}{% endblock content %}
</body>
</html>

View File

@ -1,30 +0,0 @@
{#
Copyright 2021 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 link(text, class="", uri, mode, params) %}
{% if mode == "form_post" %}
<form method="post" action="{{ uri }}">
{% for key, value in params %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
<button class="{{ class }}" type="submit">{{ text }}</button>
</form>
{% elif mode == "fragment" or mode == "query" %}
<a class="{{ class }}" href="{{ add_params_to_uri(uri=uri, mode=mode, params=params) }}">{{ text }}</a>
{% else %}
{{ throw(message="Invalid mode") }}
{% endif %}
{% endmacro %}

View File

@ -1,63 +0,0 @@
{#
Copyright 2021 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 common_class() -%}
px-4 py-2 border-2 rounded-lg font-medium text-center focus:ring-0 focus:outline-0 focus:border-black dark:focus:border-white
{%- endmacro %}
{% macro plain_error_class() -%}
{{ self::common_class() }} border-alert bg-alert text-white hover:opacity-75
{%- endmacro %}
{% macro outline_error_class() -%}
{{ self::common_class() }} border-alert text-alert hover:bg-alert/10
{%- endmacro %}
{% macro plain_class() -%}
{{ self::common_class() }} border-accent bg-accent text-white hover:opacity-75
{%- endmacro %}
{% macro outline_class() -%}
{{ self::common_class() }} border-accent hover:bg-accent/10 text-accent
{%- endmacro %}
{% macro text_class() -%}
{{ self::common_class() }} border-transparent hover:text-accent/70 text-accent
{%- endmacro %}
{% macro link(text, href="#", class="") %}
<a class="{{ self::plain_class() }} {{ class }}" href="{{ href }}">{{ text }}</a>
{% endmacro %}
{% macro link_text(text, href="#", class="") %}
<a class="{{ self::text_class() }} {{ class }}" href="{{ href }}">{{ text }}</a>
{% endmacro %}
{% macro link_outline(text, href="#", class="") %}
<a class="{{ self::outline_class() }} {{ class }}" href="{{ href }}">{{ text }}</a>
{% endmacro %}
{% macro button(text, name="", type="submit", class="", value="") %}
<button name="{{ name }}" value="{{ value }}" type="{{ type }}" class="{{ self::plain_class() }} {{ class }}">{{ text }}</button>
{% endmacro %}
{% macro button_text(text, name="", type="submit", class="", value="") %}
<button name="{{ name }}" value="{{ value }}" type="{{ type }}" class="{{ self::text_class() }} {{ class }}">{{ text }}</button>
{% endmacro %}
{% macro button_outline(text, name="", type="submit", class="", value="") %}
<button name="{{ name }}" value="{{ value }}" type="{{ type }}" class="{{ self::outline_class() }} {{ class }}">{{ text }}</button>
{% endmacro %}

View File

@ -1,25 +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 form_error_message(error) -%}
{% if error.kind == "invalid_credentials" %}
Invalid credentials
{% elif error.kind == "password_mismatch" %}
Password fields don't match
{% else %}
{{ error.kind }}
{% endif %}
{%- endmacro %}

View File

@ -1,62 +0,0 @@
{#
Copyright 2021, 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 input(label, name, type="text", form_state=false, autocomplete=false, class="", inputmode="text", autocorrect=false, autocapitalize=false) %}
{% if not form_state %}
{% set form_state = dict(errors=[], fields=dict()) %}
{% endif %}
{% set state = form_state.fields[name] | default(value=dict(errors=[], value="")) %}
{% if state.errors is not empty %}
{% set border_color = "border-alert" %}
{% set text_color = "text-alert" %}
{% else %}
{% set border_color = "border-grey-50 dark:border-grey-450" %}
{% set text_color = "text-black-800 dark:text-grey-300" %}
{% endif %}
<label class="flex flex-col block {{ class }}">
<div class="mx-2 -mb-3 -mt-2 leading-5 px-1 z-10 self-start bg-white dark:bg-black-900 border-white border-1 dark:border-2 dark:border-black-900 rounded-full text-sm {{ text_color }}">{{ label }}</div>
<input name="{{ name }}"
class="z-0 px-3 py-2 bg-white dark:bg-black-900 rounded-lg {{ border_color }} border-1 dark:border-2 focus:border-accent focus:ring-0 focus:outline-0"
type="{{ type }}"
inputmode="{{ inputmode }}"
{% if autocomplete %} autocomplete="{{ autocomplete }}" {% endif %}
{% if state.value %} value="{{ state.value }}" {% endif %}
{% if autocorrect %} autocorrect="{{ autocorrect }}" {% endif %}
{% if autocapitalize %} autocapitalize="{{ autocapitalize }}" {% endif %}
/>
{% if state.errors is not empty %}
{% for error in state.errors %}
{% if error.kind != "unspecified" %}
<div class="mx-4 text-sm text-alert">
{% if error.kind == "required" %}
This field is required
{% elif error.kind == "exists" and name == "username" %}
This username is already taken
{% elif error.kind == "policy" %}
Denied by policy: {{ error.message }}
{% else %}
{{ error.kind }}
{% endif %}
</div>
{% endif %}
{% endfor %}
{% endif %}
</label>
{% endmacro input %}

View File

@ -1,28 +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 button(text, class="", csrf_token, post_logout_action=false) %}
<form method="POST" action="/logout" class="inline">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{% if post_logout_action %}
{% for key, value in post_logout_action %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
{% endif %}
<button class="{{ class }}" type="submit">{{ text }}</button>
</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-2" 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">
Signed in as <span class="font-bold">{{ current_session.user.username }}</span>.
</div>
{{ button::link(text="My account", href="/account") }}
{{ logout::button(text="Sign out", class=button::outline_class(), csrf_token=csrf_token) }}
{% else %}
{{ button::link(text="Sign in", href="/login") }}
{{ button::link_outline(text="Create an account", href="/register") }}
{% endif %}
</div>
</nav>
{% endmacro top %}

View File

@ -1,23 +0,0 @@
{#
Copyright 2021, 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.
#}
Hi <b>{{ user.username }}</b>,<br />
<br />
your email verification code is:
<br />
<strong>{{ verification.code }}</strong><br />
<br />
kthxbye

View File

@ -1,17 +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.
#}
Your auth service verification code is: {{ verification.code }}

View File

@ -1,23 +0,0 @@
{#
Copyright 2021, 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.
#}
Hi {{ user.username }},
your email verification code is:
{{ verification.code }}
kthxbye

View File

@ -1,31 +0,0 @@
{#
Copyright 2021 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.
#}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting to client</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body onload="javascript:document.forms[0].submit()">
<form method="post" action="{{ redirect_uri }}">
{% for key, value in params %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
</form>
</body>
</html>

View File

@ -1,39 +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.
#}
{% 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 m-2">
<div class="text-center">
<h1 class="text-lg text-center font-medium">Add an email address</h1>
</div>
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-alert font-medium">
{{ errors::form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field::input(label="Email", name="email", type="email", form_state=form, autocomplete="email") }}
{{ button::button(text="Next") }}
</section>
{% endblock content %}

View File

@ -1,60 +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.
#}
{% extends "base.html" %}
{% block content %}
{{ navbar::top() }}
{% if current_session.user.primary_email %}
{% set primary_email = current_session.user.primary_email.email %}
{% else %}
{% set primary_email = "" %}
{% endif %}
<section class="container mx-auto grid gap-4 grid-cols-1 md:grid-cols-2 xl:grid-cols-3 p-2">
<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-bold xl:col-span-2">Add email</h2>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field::input(label="New email", name="email", type="email", autocomplete="email", class="xl:col-span-2") }}
{{ button::button(text="Add email", type="submit", class="xl:col-span-2 place-self-end", name="action", value="add") }}
</form>
<div class="rounded border-2 border-grey-50 dark:border-grey-450 xl:col-span-2 p-4">
<h2 class="text-xl font-bold xl:col-span-3">Emails</h2>
{% for item in emails %}
<form class="flex my-2 items-center justify-items-center" method="POST">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<input type="hidden" name="data" value="{{ item.data }}" />
<div class="font-bold flex-1">{{ item.email }}</div>
{% if item.confirmed_at %}
<div class="mr-4">Verified</div>
{% else %}
{{ button::button(text="Resend verification", type="submit", name="action", value="resend_confirmation", class="mr-4") }}
{% endif %}
{% if item.email == primary_email %}
<div class="mr-4">Primary</div>
{% else %}
{{ button::button(text="Set as primary", type="submit", name="action", value="set_primary", class="mr-4") }}
{% endif %}
{{ button::button(text="Delete", type="submit", name="action", value="remove") }}
</form>
{% endfor %}
</div>
</section>
{% endblock content %}

View File

@ -1,40 +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.
#}
{% 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 m-2">
<div class="text-center">
<h1 class="text-lg text-center font-medium">Email verification</h1>
<p>Please enter the 6-digit code sent to: <span class="font-bold">{{ email.email }}</span></p>
</div>
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-alert font-medium">
{{ errors::form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field::input(label="Code", name="code", form_state=form, autocomplete="one-time-code", inputmode="numeric") }}
{{ button::button(text="Submit") }}
</section>
{% endblock content %}

View File

@ -1,59 +0,0 @@
{#
Copyright 2021-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.
#}
{% 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 p-2">
<div 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">
<h1 class="text-2xl font-bold xl:col-span-2">Manage my account</h1>
<div class="font-bold">Your username</div>
<div>{{ current_session.user.username }}</div>
<div class="font-bold">Unique identifier</div>
<div>{{ current_session.user.sub }}</div>
<div class="font-bold">Active sessions</div>
<div>{{ active_sessions }}</div>
{% if current_session.user.primary_email %}
<div class="font-bold">Primary email</div>
<div>{{ current_session.user.primary_email.email }}</div>
{% endif %}
{{ button::link_outline(text="Change password", href="/account/password", class="col-span-2 place-self-end") }}
</div>
<div 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">
<h2 class="text-xl font-bold xl:col-span-2">Current session</h2>
<div class="font-bold">Started at</div>
<div>{{ current_session.created_at | date(format="%Y-%m-%d %H:%M:%S") }}</div>
<div class="font-bold">Last authentication</div>
<div>
{% if current_session.last_authentication %}
{{ current_session.last_authentication.created_at | date(format="%Y-%m-%d %H:%M:%S") }}
{% else %}
Never
{% endif %}
</div>
{{ button::link_outline(text="Revalidate", href="/reauth", class="col-span-2 place-self-end") }}
</div>
<div 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">
<h2 class="text-xl font-bold xl:col-span-2">Emails</h2>
{% for email in emails %}
<div class="font-bold">{{ email.email }}</div>
<div>{% if email.confirmed_at %}Confirmed{% else %}Unconfirmed{% endif %}</div>
{% endfor %}
{{ button::link_outline(text="Manage", href="/account/emails", class="col-span-2 place-self-end") }}
</div>
</section>
{% endblock content %}

View File

@ -1,32 +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.
#}
{% 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 p-2">
<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-bold xl:col-span-2">Change my password</h2>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field::input(label="Current password", name="current_password", type="password", autocomplete="current-password", class="xl:col-span-2") }}
{{ field::input(label="New password", name="new_password", type="password", autocomplete="new-password") }}
{{ field::input(label="Confirm password", name="new_password_confirm", type="password", autocomplete="new-password") }}
{{ button::button(text="Change password", type="submit", class="xl:col-span-2 place-self-end") }}
</form>
</section>
{% endblock content %}

View File

@ -1,91 +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.
#}
{% extends "base.html" %}
{% block content %}
<section class="flex items-center justify-center flex-1">
<div class="w-96 m-2">
<form method="POST" class="grid grid-cols-1 gap-6">
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col">
<div class="text-center">
<div class="bg-white rounded w-16 h-16 overflow-hidden mx-auto">
{% if grant.client.logo_uri %}
<img class="w-16 h-16" src="{{ grant.client.logo_uri }}" />
{% endif %}
</div>
<h1 class="text-lg text-center font-medium"><a target="_blank" href="{{ grant.client.client_uri }}" class="text-accent">{{ grant.client.client_name | default(value=grant.client.client_id) }}</a></h1>
<h1>at {{ grant.redirect_uri }}</h1>
<h1>wants to access your Matrix account</h1>
</div>
<div class="flex items-center m-2">
<div class="px-4 flex-1">
<p>This will allow <a target="_blank" href="{{ grant.client.client_uri }}" class="text-accent">{{ grant.client.client_name | default(value=grant.client.client_id) }}</a> to:</p>
<p class="my-2">
<ul class="list-disc">
{% for scope in grant.scope | split(pat=" ") %}
{% if scope == "openid" %}
<li>See your profile info and contact details</li>
{% elif scope is matching("^urn:matrix:org.matrix.msc2967.client:device:") %}
<li>View your existing messages and data</li>
<li>Send new messages on your behalf</li>
{% else %}
<li>{{ scope }}</li>
{% endif %}
{% endfor %}
</ul>
</p>
<p class="font-bold my-2">Make sure that you trust {{ grant.client.client_name }}</p>
<p>
You may be sharing sensitive information with this site or app.
{% if grant.client.policy_uri or grant.client.tos_uri %}
Find out how {{ grant.client.client_name }} will handle your data by reviewing it's
{% if grant.client.policy_uri %}
<a target="_blank" href="{{ grant.client.policy_uri }}" class="text-accent">privacy policy</a>{% if not grant.client.tos_uri %}.{% endif %}
{% endif %}
{% if grant.client.policy_uri and grant.client.tos_uri%}
and
{% endif %}
{% if grant.client.tos_uri %}
<a target="_blank" href="{{ grant.client.tos_uri }}" class="text-accent">terms of service</a>.
{% endif %}
{% endif %}
</p>
</div>
</div>
</div>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<div class="grid grid-cols-2 gap-4">
{{ back_to_client::link(
text="Cancel",
class=button::outline_error_class(),
uri=grant.redirect_uri,
mode=grant.response_mode,
params=dict(error="access_denied", state=grant.state)
) }}
{{ button::button(text="Allow") }}
</div>
</form>
<div class="text-center mt-4">
Not {{ current_session.user.username }}?
{{ logout::button(text="Sign out", class=button::text_class(), csrf_token=csrf_token, post_logout_action=action) }}
</div>
</div>
</section>
{% endblock content %}

View File

@ -1,39 +0,0 @@
{#
Copyright 2021 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.
#}
{% extends "base.html" %}
{% block content %}
<section class="hero is-danger">
<div class="hero-body">
<div class="container">
{% if code %}
<p class="title">
{{ code }}
</p>
{% endif %}
{% if description %}
<p class="subtitle">
{{ description }}
</p>
{% endif %}
{% if details %}
<pre><code>{{ details }}</code></pre>
{% endif %}
</div>
</div>
</section>
{% endblock %}

View File

@ -1,30 +0,0 @@
{#
Copyright 2021 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.
#}
{% extends "base.html" %}
{% block content %}
{{ navbar::top() }}
<section class="flex-1 flex flex-col items-center justify-center">
<div>
<h1 class="my-2 text-5xl font-extrabold leading-tight">Matrix Authentication Service</h1>
<p class="text-lg">
OpenID Connect discovery document:
<a class="text-links hover:text-links/70" href="{{ discovery_url }}">{{ discovery_url }}</a>
</p>
</div>
</section>
{% endblock content %}

View File

@ -1,59 +0,0 @@
{#
Copyright 2021, 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.
#}
{% 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 m-2">
<div class="text-center">
<h1 class="text-lg text-center font-medium">Sign in</h1>
<p>Please sign in to continue:</p>
</div>
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-alert font-medium">
{{ errors::form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field::input(label="Username", name="username", form_state=form, autocomplete="username", autocorrect="off", autocapitalize="none") }}
{{ field::input(label="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",
class=button::outline_error_class(),
uri=next.grant.redirect_uri,
mode=next.grant.response_mode,
params=dict(error="access_denied", state=next.grant.state)
) }}
{{ button::button(text="Next") }}
</div>
{% else %}
<div class="grid grid-cols-1 gap-4">
{{ button::button(text="Next") }}
</div>
{% endif %}
<div class="text-center mt-4">
Don't have an account yet?
{{ button::link_text(text="Create an account", href=register_link) }}
</div>
</form>
</section>
{% endblock content %}

View File

@ -1,53 +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.
#}
{% extends "base.html" %}
{% block content %}
<section class="flex items-center justify-center flex-1">
<div class="w-96 m-2">
<div class="grid grid-cols-1 gap-6">
<h1 class="text-xl font-bold">The authorization request was denied the policy enforced by this service.</h1>
<p>This might be because of the client which authored the request, the currently logged in user, or the request itself.</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 grant.client.logo_uri %}
<img class="w-16 h-16" src="{{ grant.client.logo_uri }}" />
{% endif %}
</div>
<h1 class="text-lg text-center font-medium flex-1"><a target="_blank" href="{{ grant.client.client_uri }}" class="text-accent">{{ grant.client.client_name | default(value=grant.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">
Logged as <span class="font-bold">{{ current_session.user.username }}</span>
</div>
{{ logout::button(text="Sign out", class=button::plain_error_class(), csrf_token=csrf_token, post_logout_action=action) }}
</div>
{{ back_to_client::link(
text="Cancel",
class=button::outline_error_class(),
uri=grant.redirect_uri,
mode=grant.response_mode,
params=dict(error="access_denied", state=grant.state)
) }}
</div>
</div>
</section>
{% endblock content %}

View File

@ -1,67 +0,0 @@
{#
Copyright 2021 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.
#}
{% extends "base.html" %}
{% block content %}
<section class="flex items-center justify-center flex-1">
<div class="w-96 m-4">
<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="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",
class=button::outline_error_class(),
uri=next.grant.redirect_uri,
mode=next.grant.response_mode,
params=dict(error="access_denied", state=next.grant.state)
) }}
{{ button::button(text="Next") }}
</div>
{% else %}
<div class="grid grid-cols-1 gap-4">
{{ button::button(text="Next") }}
</div>
{% endif %}
</form>
<div class="text-center mt-4">
Not {{ current_session.user.username }}?
{{ logout::button(text="Sign out", class=button::text_class(), csrf_token=csrf_token, post_logout_action=action) }}
</div>
</div>
</section>
<!-- <div class="flex justify-center">
<div class="w-96 m-2">
<h3 class="title is-3">Current session data:</h3>
<pre class="text-sm whitespace-pre-wrap"><code>{{ current_session | json_encode(pretty=True) | safe }}</code></pre>
</div>
{% if next %}
<div class="w-96 m-2">
<h3 class="title is-3">Next action:</h3>
<pre class="text-sm whitespace-pre-wrap"><code>{{ next | json_encode(pretty=True) | safe }}</code></pre>
</div>
{% endif %}
</div> -->
{% endblock content %}

View File

@ -1,64 +0,0 @@
{#
Copyright 2021, 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.
#}
{% 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">
<div class="text-center">
<h1 class="text-lg text-center font-medium">Create an account</h1>
<p>Please create an account to get started:</p>
</div>
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-alert font-medium">
{{ errors::form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ field::input(label="Username", name="username", form_state=form, autocomplete="username", autocorrect="off", autocapitalize="none") }}
{{ field::input(label="Email", name="email", type="email", form_state=form, autocomplete="email") }}
{{ field::input(label="Password", name="password", type="password", form_state=form, autocomplete="new-password") }}
{{ field::input(label="Confirm Password", 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="Cancel",
class=button::outline_error_class(),
uri=next.grant.redirect_uri,
mode=next.grant.response_mode,
params=dict(error="access_denied", state=next.grant.state)
) }}
{{ button::button(text="Next") }}
</div>
{% else %}
<div class="grid grid-cols-1 gap-4">
{{ button::button(text="Next") }}
</div>
{% endif %}
<div class="text-center mt-4">
Already have an account?
{# TODO: proper link #}
{{ button::link_text(text="Sign in instead", href=login_link) }}
</div>
</form>
</section>
{% endblock content %}

View File

@ -1,40 +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.
#}
{% extends "base.html" %}
{% block content %}
<section class="flex items-center justify-center flex-1">
<div class="w-96 m-2">
<form method="POST" class="grid grid-cols-1 gap-6">
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex flex-col">
<div class="text-center">
<h1 class="text-lg text-center font-medium">{{ login.redirect_uri }}</h1>
<h1>wants to access your Matrix account</h1>
</div>
</div>
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{{ button::button(text="Allow") }}
</form>
<div class="text-center mt-4">
Not {{ current_session.user.username }}?
{{ logout::button(text="Sign out", class=button::text_class(), csrf_token=csrf_token, post_logout_action=action) }}
</div>
</div>
</section>
{% endblock content %}