1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-21 23:00:50 +03:00

Move templates to their own crate

This commit is contained in:
Quentin Gliech
2021-10-18 17:40:25 +02:00
parent cf8793da27
commit 026bc47c27
33 changed files with 319 additions and 156 deletions

View File

@@ -14,6 +14,7 @@ futures-util = "0.3.17"
# Logging and tracing
tracing = "0.1.29"
opentelemetry = "0.16.0"
# Error management
thiserror = "1.0.30"
@@ -63,12 +64,12 @@ rand = "0.8.4"
bincode = "1.3.3"
headers = "0.3.4"
cookie = "0.15.1"
once_cell = "1.8.0"
oauth2-types = { path = "../oauth2-types", features = ["sqlx_type"] }
mas-config = { path = "../config" }
mas-data-model = { path = "../data-model" }
opentelemetry = "0.16.0"
once_cell = "1.8.0"
mas-templates = { path = "../templates" }
[dependencies.jwt-compact]
# Waiting on the next release because of the bump of the `rsa` dependency

View File

@@ -12,9 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{collections::HashMap, fmt::Debug, hash::Hash};
use serde::{ser::SerializeMap, Serialize};
use warp::{reject::Reject, Rejection};
#[derive(Debug)]
@@ -38,105 +35,3 @@ where
self.map_err(|e| warp::reject::custom(WrappedError(e.into())))
}
}
pub trait HtmlError: Debug + Send + Sync + 'static {
fn html_display(&self) -> String;
}
pub trait WrapFormError<FieldType> {
fn on_form(self) -> ErroredForm<FieldType>;
fn on_field(self, field: FieldType) -> ErroredForm<FieldType>;
}
impl<E, FieldType> WrapFormError<FieldType> for E
where
E: HtmlError,
{
fn on_form(self) -> ErroredForm<FieldType> {
let mut f = ErroredForm::new();
f.form.push(FormError {
error: Box::new(self),
});
f
}
fn on_field(self, field: FieldType) -> ErroredForm<FieldType> {
let mut f = ErroredForm::new();
f.fields.push(FieldError {
field,
error: Box::new(self),
});
f
}
}
#[derive(Debug)]
struct FormError {
error: Box<dyn HtmlError>,
}
impl Serialize for FormError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.error.html_display())
}
}
#[derive(Debug)]
struct FieldError<FieldType> {
field: FieldType,
error: Box<dyn HtmlError>,
}
#[derive(Debug)]
pub struct ErroredForm<FieldType> {
form: Vec<FormError>,
fields: Vec<FieldError<FieldType>>,
}
impl<T> Default for ErroredForm<T> {
fn default() -> Self {
Self {
form: Vec::new(),
fields: Vec::new(),
}
}
}
impl<T> ErroredForm<T> {
#[must_use]
pub fn new() -> Self {
Self {
form: Vec::new(),
fields: Vec::new(),
}
}
}
impl<T> Reject for ErroredForm<T> where T: Debug + Send + Sync + 'static {}
impl<FieldType: Copy + Serialize + Hash + Eq> Serialize for ErroredForm<FieldType> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(2))?;
let has_errors = !self.form.is_empty() || !self.fields.is_empty();
map.serialize_entry("has_errors", &has_errors)?;
map.serialize_entry("form_errors", &self.form)?;
let fields: HashMap<FieldType, Vec<String>> =
self.fields.iter().fold(HashMap::new(), |mut map, err| {
map.entry(err.field)
.or_default()
.push(err.error.html_display());
map
});
map.serialize_entry("fields_errors", &fields)?;
map.end()
}
}

View File

@@ -23,8 +23,13 @@ static PROPAGATOR_HEADERS: OnceCell<Vec<String>> = OnceCell::new();
/// Notify the CORS filter what opentelemetry propagators are being used. This
/// helps whitelisting headers in CORS requests.
pub fn set_propagator(propagator: &dyn opentelemetry::propagation::TextMapPropagator) {
let headers = propagator.fields().map(ToString::to_string).collect();
tracing::debug!(
?headers,
"Headers allowed in CORS requests for trace propagators set"
);
PROPAGATOR_HEADERS
.set(propagator.fields().map(ToString::to_string).collect())
.set(headers)
.expect(concat!(module_path!(), "::set_propagator was called twice"));
}

View File

@@ -28,13 +28,11 @@ pub mod session;
use std::convert::Infallible;
use mas_templates::Templates;
use warp::{Filter, Rejection};
pub use self::csrf::CsrfToken;
use crate::{
config::{KeySet, OAuth2Config},
templates::Templates,
};
use crate::config::{KeySet, OAuth2Config};
/// Get the [`Templates`]
#[must_use]

View File

@@ -14,10 +14,11 @@
#![allow(clippy::unused_async)] // Some warp filters need that
use mas_templates::Templates;
use sqlx::PgPool;
use warp::{Filter, Rejection, Reply};
use crate::{config::RootConfig, templates::Templates};
use crate::config::RootConfig;
mod health;
mod oauth2;

View File

@@ -25,6 +25,7 @@ use hyper::{
};
use itertools::Itertools;
use mas_data_model::BrowserSession;
use mas_templates::{FormPostContext, Templates};
use oauth2_types::{
errors::{ErrorResponse, InvalidRequest, OAuth2Error},
pkce,
@@ -62,7 +63,6 @@ use crate::{
},
PostgresqlBackend,
},
templates::{FormPostContext, Templates},
tokens::{AccessToken, RefreshToken},
};

View File

@@ -12,13 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use mas_templates::Templates;
use sqlx::PgPool;
use warp::{Filter, Rejection, Reply};
use crate::{
config::{CookiesConfig, OAuth2Config},
templates::Templates,
};
use crate::config::{CookiesConfig, OAuth2Config};
mod authorization;
mod discovery;

View File

@@ -13,6 +13,7 @@
// limitations under the License.
use mas_data_model::BrowserSession;
use mas_templates::{IndexContext, TemplateContext, Templates};
use sqlx::PgPool;
use url::Url;
use warp::{reply::html, Filter, Rejection, Reply};
@@ -26,7 +27,6 @@ use crate::{
with_templates, CsrfToken,
},
storage::PostgresqlBackend,
templates::{IndexContext, TemplateContext, Templates},
};
pub(super) fn filter(
@@ -56,7 +56,7 @@ async fn get(
) -> Result<impl Reply, Rejection> {
let ctx = IndexContext::new(discovery_url)
.maybe_with_session(maybe_session)
.with_csrf(&csrf_token);
.with_csrf(csrf_token.form_value());
let content = templates.render_index(&ctx)?;
let reply = html(content);

View File

@@ -15,14 +15,15 @@
use std::convert::TryFrom;
use hyper::http::uri::{Parts, PathAndQuery, Uri};
use mas_data_model::BrowserSession;
use mas_data_model::{errors::WrapFormError, BrowserSession};
use mas_templates::{LoginContext, LoginFormField, TemplateContext, Templates};
use serde::{Deserialize, Serialize};
use sqlx::{pool::PoolConnection, PgPool, Postgres};
use warp::{reply::html, Filter, Rejection, Reply};
use crate::{
config::{CookiesConfig, CsrfConfig},
errors::{WrapError, WrapFormError},
errors::WrapError,
filters::{
cookies::{encrypted_cookie_saver, EncryptedCookieSaver},
csrf::{protected_form, updated_csrf_token},
@@ -31,7 +32,6 @@ use crate::{
with_templates, CsrfToken,
},
storage::{login, PostgresqlBackend},
templates::{LoginContext, LoginFormField, TemplateContext, Templates},
};
#[derive(Serialize, Deserialize)]
@@ -114,7 +114,7 @@ async fn get(
if maybe_session.is_some() {
Ok(Box::new(query.redirect()?))
} else {
let ctx = LoginContext::default().with_csrf(&csrf_token);
let ctx = LoginContext::default().with_csrf(csrf_token.form_value());
let content = templates.render_login(&ctx)?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;
@@ -145,7 +145,8 @@ async fn post(
LoginError::Authentication { .. } => e.on_field(LoginFormField::Password),
LoginError::Other(_) => e.on_form(),
};
let ctx = LoginContext::with_form_error(errored_form).with_csrf(&csrf_token);
let ctx =
LoginContext::with_form_error(errored_form).with_csrf(csrf_token.form_value());
let content = templates.render_login(&ctx)?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;

View File

@@ -12,13 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use mas_templates::Templates;
use sqlx::PgPool;
use warp::{Filter, Rejection, Reply};
use crate::{
config::{CookiesConfig, CsrfConfig, OAuth2Config},
templates::Templates,
};
use crate::config::{CookiesConfig, CsrfConfig, OAuth2Config};
mod index;
mod login;

View File

@@ -13,6 +13,7 @@
// limitations under the License.
use mas_data_model::BrowserSession;
use mas_templates::{EmptyContext, TemplateContext, Templates};
use serde::Deserialize;
use sqlx::{PgPool, Postgres, Transaction};
use warp::{hyper::Uri, reply::html, Filter, Rejection, Reply};
@@ -28,7 +29,6 @@ use crate::{
with_templates, CsrfToken,
},
storage::{user::authenticate_session, PostgresqlBackend},
templates::{EmptyContext, TemplateContext, Templates},
};
#[derive(Deserialize, Debug)]
@@ -64,7 +64,9 @@ async fn get(
csrf_token: CsrfToken,
session: BrowserSession<PostgresqlBackend>,
) -> Result<impl Reply, Rejection> {
let ctx = EmptyContext.with_session(session).with_csrf(&csrf_token);
let ctx = EmptyContext
.with_session(session)
.with_csrf(csrf_token.form_value());
let content = templates.render_reauth(&ctx)?;
let reply = html(content);

View File

@@ -17,6 +17,7 @@ use std::convert::TryFrom;
use argon2::Argon2;
use hyper::http::uri::{Parts, PathAndQuery, Uri};
use mas_data_model::BrowserSession;
use mas_templates::{EmptyContext, TemplateContext, Templates};
use serde::{Deserialize, Serialize};
use sqlx::{pool::PoolConnection, PgPool, Postgres};
use warp::{reply::html, Filter, Rejection, Reply};
@@ -32,7 +33,6 @@ use crate::{
with_templates, CsrfToken,
},
storage::{register_user, user::start_session, PostgresqlBackend},
templates::{EmptyContext, TemplateContext, Templates},
};
#[derive(Serialize, Deserialize)]
@@ -116,7 +116,7 @@ async fn get(
if maybe_session.is_some() {
Ok(Box::new(query.redirect()?))
} else {
let ctx = EmptyContext.with_csrf(&csrf_token);
let ctx = EmptyContext.with_csrf(csrf_token.form_value());
let content = templates.render_register(&ctx)?;
let reply = html(content);
let reply = cookie_saver.save_encrypted(&csrf_token, reply)?;

View File

@@ -29,5 +29,4 @@ pub mod handlers;
pub mod reply;
pub mod storage;
pub mod tasks;
pub mod templates;
pub mod tokens;

View File

@@ -16,7 +16,7 @@
#![allow(clippy::used_underscore_binding)] // This is needed by sqlx macros
use mas_data_model::StorageBackend;
use mas_data_model::{StorageBackend, StorageBackendMarker};
use serde::Serialize;
use sqlx::migrate::Migrator;
use thiserror::Error;
@@ -38,6 +38,8 @@ impl StorageBackend for PostgresqlBackend {
type UserData = i64;
}
impl StorageBackendMarker for PostgresqlBackend {}
pub mod oauth2;
pub mod user;

View File

@@ -17,7 +17,7 @@ use std::{borrow::BorrowMut, convert::TryInto};
use anyhow::Context;
use argon2::Argon2;
use chrono::{DateTime, Utc};
use mas_data_model::{Authentication, BrowserSession, User};
use mas_data_model::{errors::HtmlError, Authentication, BrowserSession, User};
use password_hash::{PasswordHash, PasswordHasher, SaltString};
use rand::rngs::OsRng;
use sqlx::{Acquire, Executor, FromRow, Postgres, Transaction};
@@ -27,7 +27,6 @@ use tracing::{info_span, Instrument};
use warp::reject::Reject;
use super::{DatabaseInconsistencyError, PostgresqlBackend};
use crate::errors::HtmlError;
#[derive(Debug, Clone, FromRow)]
struct UserLookup {

View File

@@ -1,332 +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.
//! Contexts used in templates
use mas_data_model::BrowserSession;
use oauth2_types::errors::OAuth2Error;
use serde::{ser::SerializeStruct, Serialize};
use url::Url;
use crate::{errors::ErroredForm, filters::CsrfToken, storage::PostgresqlBackend};
/// Helper trait to construct context wrappers
pub trait TemplateContext {
/// Attach a user session to the template context
fn with_session(self, current_session: BrowserSession<PostgresqlBackend>) -> WithSession<Self>
where
Self: Sized,
{
WithSession {
current_session,
inner: self,
}
}
/// Attach an optional user session to the template context
fn maybe_with_session(
self,
current_session: Option<BrowserSession<PostgresqlBackend>>,
) -> WithOptionalSession<Self>
where
Self: Sized,
{
WithOptionalSession {
current_session,
inner: self,
}
}
/// Attach a CSRF token to the template context
fn with_csrf(self, token: &CsrfToken) -> WithCsrf<Self>
where
Self: Sized,
{
WithCsrf {
csrf_token: token.form_value(),
inner: self,
}
}
/// Generate sample values for this context type
///
/// This is then used to check for template validity in unit tests and in
/// the CLI (`cargo run -- templates check`)
fn sample() -> Vec<Self>
where
Self: Sized;
}
/// Context with a CSRF token in it
#[derive(Serialize)]
pub struct WithCsrf<T> {
csrf_token: String,
#[serde(flatten)]
inner: T,
}
impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
fn sample() -> Vec<Self>
where
Self: Sized,
{
T::sample()
.into_iter()
.map(|inner| WithCsrf {
csrf_token: "fake_csrf_token".into(),
inner,
})
.collect()
}
}
/// Context with a user session in it
#[derive(Serialize)]
pub struct WithSession<T> {
current_session: BrowserSession<PostgresqlBackend>,
#[serde(flatten)]
inner: T,
}
impl<T: TemplateContext> TemplateContext for WithSession<T> {
fn sample() -> Vec<Self>
where
Self: Sized,
{
BrowserSession::<PostgresqlBackend>::samples()
.into_iter()
.flat_map(|session| {
T::sample().into_iter().map(move |inner| WithSession {
current_session: session.clone(),
inner,
})
})
.collect()
}
}
/// Context with an optional user session in it
#[derive(Serialize)]
pub struct WithOptionalSession<T> {
current_session: Option<BrowserSession<PostgresqlBackend>>,
#[serde(flatten)]
inner: T,
}
impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
fn sample() -> Vec<Self>
where
Self: Sized,
{
BrowserSession::<PostgresqlBackend>::samples()
.into_iter()
.map(Some) // Wrap all samples in an Option
.chain(std::iter::once(None)) // Add the "None" option
.flat_map(|session| {
T::sample()
.into_iter()
.map(move |inner| WithOptionalSession {
current_session: session.clone(),
inner,
})
})
.collect()
}
}
/// An empty context used for composition
pub struct EmptyContext;
impl Serialize for EmptyContext {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut s = serializer.serialize_struct("EmptyContext", 0)?;
// FIXME: for some reason, serde seems to not like struct flattening with empty
// stuff
s.serialize_field("__UNUSED", &())?;
s.end()
}
}
impl TemplateContext for EmptyContext {
fn sample() -> Vec<Self>
where
Self: Sized,
{
vec![EmptyContext]
}
}
/// Context used by the `index.html` template
#[derive(Serialize)]
pub struct IndexContext {
discovery_url: Url,
}
impl IndexContext {
#[must_use]
pub fn new(discovery_url: Url) -> Self {
Self { discovery_url }
}
}
impl TemplateContext for IndexContext {
fn sample() -> Vec<Self>
where
Self: Sized,
{
vec![Self {
discovery_url: "https://example.com/.well-known/openid-configuration"
.parse()
.unwrap(),
}]
}
}
#[derive(Serialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum LoginFormField {
Username,
Password,
}
/// Context used by the `login.html` template
#[derive(Serialize)]
pub struct LoginContext {
form: ErroredForm<LoginFormField>,
}
impl TemplateContext for LoginContext {
fn sample() -> Vec<Self>
where
Self: Sized,
{
// TODO: samples with errors
vec![LoginContext {
form: ErroredForm::default(),
}]
}
}
impl LoginContext {
#[must_use]
pub fn with_form_error(form: ErroredForm<LoginFormField>) -> Self {
Self { form }
}
}
impl Default for LoginContext {
fn default() -> Self {
Self {
form: ErroredForm::new(),
}
}
}
/// Context used by the `form_post.html` template
#[derive(Serialize)]
pub struct FormPostContext<T> {
redirect_uri: Url,
params: T,
}
impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
fn sample() -> Vec<Self>
where
Self: Sized,
{
let sample_params = T::sample();
sample_params
.into_iter()
.map(|params| FormPostContext {
redirect_uri: "https://example.com/callback".parse().unwrap(),
params,
})
.collect()
}
}
impl<T> FormPostContext<T> {
pub fn new(redirect_uri: Url, params: T) -> Self {
Self {
redirect_uri,
params,
}
}
}
/// Context used by the `error.html` template
#[derive(Default, Serialize)]
pub struct ErrorContext {
code: Option<&'static str>,
description: Option<String>,
details: Option<String>,
}
impl TemplateContext for ErrorContext {
fn sample() -> Vec<Self>
where
Self: Sized,
{
vec![
Self::new()
.with_code("sample_error")
.with_description("A fancy description".into())
.with_details("Something happened".into()),
Self::new().with_code("another_error"),
Self::new(),
]
}
}
impl ErrorContext {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_code(mut self, code: &'static str) -> Self {
self.code = Some(code);
self
}
#[must_use]
pub fn with_description(mut self, description: String) -> Self {
self.description = Some(description);
self
}
#[allow(dead_code)]
#[must_use]
pub fn with_details(mut self, details: String) -> Self {
self.details = Some(details);
self
}
}
impl From<Box<dyn OAuth2Error>> for ErrorContext {
fn from(err: Box<dyn OAuth2Error>) -> Self {
let mut ctx = ErrorContext::new().with_code(err.error());
if let Some(desc) = err.description() {
ctx = ctx.with_description(desc);
}
ctx
}
}

View File

@@ -1,102 +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.
/// Count the number of tokens. Used to have a fixed-sized array for the
/// templates list.
macro_rules! count {
() => (0_usize);
( $x:tt $($xs:tt)* ) => (1_usize + count!($($xs)*));
}
/// Macro that helps generating helper function that renders a specific template
/// with a strongly-typed context. It also register the template in a static
/// array to help detecting missing templates at startup time.
///
/// The syntax looks almost like a function to confuse syntax highlighter as
/// little as possible.
#[macro_export]
macro_rules! register_templates {
{
$(
extra = { $( $extra_template:expr ),* };
)?
$(
// Match any attribute on the function, such as #[doc], #[allow(dead_code)], etc.
$( #[ $attr:meta ] )*
// The function name
pub fn $name:ident
// Optional list of generics. Taken from
// https://newbedev.com/rust-macro-accepting-type-with-generic-parameters
$(< $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+ >)?
// Type of context taken by the template
( $param:ty )
{
// The name of the template file
$template:expr
}
)*
} => {
/// List of registered templates
static TEMPLATES: [(&'static str, &'static str); count!( $( $template )* )] = [
$( ($template, include_str!(concat!("res/", $template))) ),*
];
/// List of extra templates used by other templates
static EXTRA_TEMPLATES: [(&'static str, &'static str); count!( $( $( $extra_template )* )? )] = [
$( $( ($extra_template, include_str!(concat!("res/", $extra_template))) ),* )?
];
impl Templates {
$(
$(#[$attr])?
pub fn $name
$(< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)?
(&self, context: &$param)
-> Result<String, TemplateError> {
let ctx = Context::from_serialize(context)
.map_err(|source| TemplateError::Context { template: $template, source })?;
self.0.render($template, &ctx)
.map_err(|source| TemplateError::Render { template: $template, source })
}
)*
}
/// Helps rendering each template with sample data
pub mod check {
use super::*;
$(
#[doc = concat!("Render the `", $template, "` template with sample contexts")]
pub fn $name
$(< $( $lt $( : $clt $(+ $dlt )* + TemplateContext )? ),+ >)?
(templates: &Templates)
-> anyhow::Result<()> {
let samples: Vec< $param > = TemplateContext::sample();
let name = $template;
for sample in samples {
let context = serde_json::to_value(&sample)?;
::tracing::info!(name, %context, "Rendering template");
templates. $name (&sample)
.with_context(|| format!("Failed to render template {:?} with context {}", name, context))?;
}
Ok(())
}
)*
}
};
}

View File

@@ -1,231 +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.
#![deny(missing_docs)]
//! Templates rendering
use std::{collections::HashSet, io::Cursor, path::Path, string::ToString, sync::Arc};
use anyhow::Context as _;
use mas_config::TemplatesConfig;
use serde::Serialize;
use tera::{Context, Error as TeraError, Tera};
use thiserror::Error;
use tokio::{fs::OpenOptions, io::AsyncWriteExt};
use tracing::{debug, info, warn};
use warp::reject::Reject;
#[allow(missing_docs)] // TODO
mod context;
#[macro_use]
mod macros;
pub use self::context::{
EmptyContext, ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField,
TemplateContext, WithCsrf, WithOptionalSession, WithSession,
};
/// Wrapper around [`tera::Tera`] helping rendering the various templates
#[derive(Debug, Clone)]
pub struct Templates(Arc<Tera>);
/// There was an issue while loading the templates
#[derive(Error, Debug)]
pub enum TemplateLoadingError {
/// Some templates failed to compile
#[error("could not load and compile some templates")]
Compile(#[from] TeraError),
/// There are essential templates missing
#[error("missing templates {missing:?}")]
MissingTemplates {
/// List of missing templates
missing: HashSet<String>,
/// List of templates that were loaded
loaded: HashSet<String>,
},
}
impl Templates {
/// Load the templates from [the config][`TemplatesConfig`]
pub fn load_from_config(config: &TemplatesConfig) -> Result<Self, TemplateLoadingError> {
Self::load(config.path.as_deref(), config.builtin)
}
/// Load the templates and check all needed templates are properly loaded
///
/// # Arguments
///
/// * `path` - An optional path to where templates should be loaded
/// * `builtin` - Set to `true` to load the builtin templates as well
pub fn load(path: Option<&str>, builtin: bool) -> Result<Self, TemplateLoadingError> {
let tera = {
let mut tera = Tera::default();
if builtin {
info!("Loading builtin templates");
for (name, source) in EXTRA_TEMPLATES {
tera.add_raw_template(name, source)?;
}
for (name, source) in TEMPLATES {
tera.add_raw_template(name, source)?;
}
}
if let Some(path) = path {
let path = format!("{}/**/*.{{html,txt}}", path);
info!(%path, "Loading templates from filesystem");
tera.extend(&Tera::parse(&path)?)?;
}
tera.build_inheritance_chains()?;
tera.check_macro_files()?;
tera
};
let loaded: HashSet<_> = tera.get_template_names().collect();
let needed: HashSet<_> = std::array::IntoIter::new(TEMPLATES)
.map(|(name, _)| name)
.collect();
debug!(?loaded, ?needed, "Templates loaded");
let missing: HashSet<_> = needed.difference(&loaded).collect();
if missing.is_empty() {
Ok(Self(Arc::new(tera)))
} else {
let missing = missing.into_iter().map(ToString::to_string).collect();
let loaded = loaded.into_iter().map(ToString::to_string).collect();
Err(TemplateLoadingError::MissingTemplates { missing, loaded })
}
}
/// Save the builtin templates to a folder
pub async fn save(path: &Path, overwrite: bool) -> anyhow::Result<()> {
tokio::fs::create_dir_all(&path)
.await
.context("could not create destination folder")?;
let templates = std::array::IntoIter::new(TEMPLATES).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 {
let path = path.join(name);
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
#[derive(Error, Debug)]
pub enum TemplateError {
/// Failed to prepare the context used by this template
#[error("could not prepare context for template {template:?}")]
Context {
/// The name of the template being rendered
template: &'static str,
/// The underlying error
#[source]
source: TeraError,
},
/// Failed to render the template
#[error("could not render template {template:?}")]
Render {
/// The name of the template being rendered
template: &'static str,
/// The underlying error
#[source]
source: TeraError,
},
}
impl Reject for TemplateError {}
register_templates! {
extra = { "base.html" };
/// Render the login page
pub fn render_login(WithCsrf<LoginContext>) { "login.html" }
/// Render the registration page
pub fn render_register(WithCsrf<EmptyContext>) { "register.html" }
/// Render the home page
pub fn render_index(WithCsrf<WithOptionalSession<IndexContext>>) { "index.html" }
/// Render the re-authentication form
pub fn render_reauth(WithCsrf<WithSession<EmptyContext>>) { "reauth.html" }
/// Render the form used by the form_post response mode
pub fn render_form_post<T: Serialize>(FormPostContext<T>) { "form_post.html" }
/// Render the HTML error page
pub fn render_error(ErrorContext) { "error.html" }
}
impl Templates {
/// Render all templates with the generated samples to check if they render
/// properly
pub fn check_render(&self) -> anyhow::Result<()> {
check::render_login(self)?;
check::render_register(self)?;
check::render_index(self)?;
check::render_reauth(self)?;
check::render_form_post::<EmptyContext>(self)?;
check::render_error(self)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn check_builtin_templates() {
let templates = Templates::load(None, true).unwrap();
templates.check_render().unwrap();
}
}

View File

@@ -1,66 +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>{% block title %}matrix-authentication-service{% endblock title %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
</head>
<body>
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item" href="/">
matrix-authentication-service
</a>
</div>
<div class="navbar-end">
{% if current_session %}
<div class="navbar-item">
Howdy {{ current_session.user.username }}!
</div>
<div class="navbar-item">
<form method="POST" action="/logout">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<button class="button is-light" action="submit">
Log out
</button>
</form>
</div>
{% else %}
<div class="navbar-item">
<a class="button is-light" href="/login">
Log in
</a>
</div>
<div class="navbar-item">
<a class="button is-light" href="/register">
Register
</a>
</div>
{% endif %}
</div>
</div>
</nav>
{% block content %}{% endblock content %}
</body>
</html>

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,27 +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.
#}
{% if code %}
{{- code }}
{% endif %}
{%- if description %}
{{ description }}
{% endif %}
{%- if details %}
{{ details }}
{% endif %}

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,28 +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="section">
<div class="container content">
<h1>Matrix Authentication Service</h1>
<p>
OpenID Connect discovery document: <a href="{{ discovery_url }}">{{ discovery_url }}</a>
</p>
</div>
</section>
{% endblock content %}

View File

@@ -1,70 +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="section">
<div class="container is-max-desktop">
<div class="columns">
<div class="column is-half is-offset-one-quarter">
{% if form.has_errors %}
<article class="message is-danger">
<div class="message-body">
{% for message in form.form_errors %}
<p>{{ message | safe }}</p>
{% else %}
<p>Login failed, check the fields below for more details.</p>
{% endfor %}
</div>
</article>
{% endif %}
<form method="POST">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<div class="field">
<label class="label" for="login-username">Username</label>
<div class="control">
<input class="input{% if 'username' in form.fields_errors %} is-danger{% endif %}" name="username" id="login-username" type="text">
</div>
{% if 'username' in form.fields_errors %}
{% for message in form.fields_errors.username %}
<p class="help is-danger">{{ message | safe }}</p>
{% endfor %}
{% endif %}
</div>
<div class="field">
<label class="label" for="login-password">Password</label>
<div class="control">
<input class="input{% if 'password' in form.fields_errors %} is-danger{% endif %}" name="password" id="login-password" type="password">
</div>
{% if 'password' in form.fields_errors %}
{% for message in form.fields_errors.password %}
<p class="help is-danger">{{ message | safe }}</p>
{% endfor %}
{% endif %}
</div>
<div class="control">
<button type="submit" class="button is-link">Login</button>
</div>
</form>
</div>
</div>
</div>
</section>
{% endblock content %}

View File

@@ -1,45 +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="section">
<div class="container is-max-desktop">
<div class="columns">
<div class="column is-one-third">
<form method="POST">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<div class="field">
<label class="label" for="login-password">Password</label>
<div class="control">
<input class="input" name="password" id="login-password" type="password">
</div>
</div>
<div class="control">
<button type="submit" class="button is-link">Submit</button>
</div>
</form>
</div>
<div class="column is-two-third">
<pre><code>{{ current_session | json_encode(pretty=True) | safe }}</code></pre>
</div>
</div>
</div>
</section>
{% endblock content %}

View File

@@ -1,55 +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="section">
<div class="container is-max-desktop">
<div class="columns">
<div class="column is-one-third">
<form method="POST">
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
<div class="field">
<label class="label" for="register-username">Username</label>
<div class="control">
<input class="input" name="username" id="register-username" type="text">
</div>
</div>
<div class="field">
<label class="label" for="register-password">Password</label>
<div class="control">
<input class="input" name="password" id="register-password" type="password">
</div>
</div>
<div class="field">
<label class="label" for="register-password">Confirm password</label>
<div class="control">
<input class="input" name="password_confirm" id="register-password-confirm" type="password">
</div>
</div>
<div class="control">
<button type="submit" class="button is-link">Register</button>
</div>
</form>
</div>
</div>
</div>
</section>
{% endblock content %}