You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Embed templates in binary & add command to export them
This commit is contained in:
@ -27,12 +27,14 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte
|
|||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
config::ConfigCommand, database::DatabaseCommand, manage::ManageCommand, server::ServerCommand,
|
config::ConfigCommand, database::DatabaseCommand, manage::ManageCommand, server::ServerCommand,
|
||||||
|
templates::TemplatesCommand,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod database;
|
mod database;
|
||||||
mod manage;
|
mod manage;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod templates;
|
||||||
|
|
||||||
#[derive(Clap, Debug)]
|
#[derive(Clap, Debug)]
|
||||||
enum Subcommand {
|
enum Subcommand {
|
||||||
@ -47,6 +49,9 @@ enum Subcommand {
|
|||||||
|
|
||||||
/// Manage the instance
|
/// Manage the instance
|
||||||
Manage(ManageCommand),
|
Manage(ManageCommand),
|
||||||
|
|
||||||
|
/// Templates-related commands
|
||||||
|
Templates(TemplatesCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clap, Debug)]
|
#[derive(Clap, Debug)]
|
||||||
@ -67,6 +72,7 @@ impl RootCommand {
|
|||||||
Some(S::Database(c)) => c.run(self).await,
|
Some(S::Database(c)) => c.run(self).await,
|
||||||
Some(S::Server(c)) => c.run(self).await,
|
Some(S::Server(c)) => c.run(self).await,
|
||||||
Some(S::Manage(c)) => c.run(self).await,
|
Some(S::Manage(c)) => c.run(self).await,
|
||||||
|
Some(S::Templates(c)) => c.run(self).await,
|
||||||
None => ServerCommand::default().run(self).await,
|
None => ServerCommand::default().run(self).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,8 @@ impl ServerCommand {
|
|||||||
let pool = config.database.connect().await?;
|
let pool = config.database.connect().await?;
|
||||||
|
|
||||||
// Load and compile the templates
|
// Load and compile the templates
|
||||||
let templates = Templates::load().context("could not load templates")?;
|
// TODO: custom template path from the config
|
||||||
|
let templates = Templates::load(None, true).context("could not load templates")?;
|
||||||
|
|
||||||
// Start the server
|
// Start the server
|
||||||
let root = mas_core::handlers::root(&pool, &templates, &config);
|
let root = mas_core::handlers::root(&pool, &templates, &config);
|
||||||
|
64
crates/cli/src/templates.rs
Normal file
64
crates/cli/src/templates.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::Clap;
|
||||||
|
use mas_core::templates::Templates;
|
||||||
|
|
||||||
|
use super::RootCommand;
|
||||||
|
|
||||||
|
#[derive(Clap, Debug)]
|
||||||
|
pub(super) struct TemplatesCommand {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
subcommand: TemplatesSubcommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clap, Debug)]
|
||||||
|
enum TemplatesSubcommand {
|
||||||
|
/// Save the builtin templates to a folder
|
||||||
|
Save {
|
||||||
|
/// Where the templates should be saved
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
/// Overwrite existing template files
|
||||||
|
#[clap(long)]
|
||||||
|
overwrite: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Check for template validity at given path.
|
||||||
|
Check {
|
||||||
|
/// Path where the templates are
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplatesCommand {
|
||||||
|
pub async fn run(&self, _root: &RootCommand) -> anyhow::Result<()> {
|
||||||
|
use TemplatesSubcommand as SC;
|
||||||
|
match &self.subcommand {
|
||||||
|
SC::Save { path, overwrite } => {
|
||||||
|
Templates::save(path, *overwrite).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
SC::Check { path } => {
|
||||||
|
Templates::load(Some(path.clone()), false)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,305 +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.
|
|
||||||
|
|
||||||
use std::{collections::HashSet, string::ToString, sync::Arc};
|
|
||||||
|
|
||||||
use oauth2_types::errors::OAuth2Error;
|
|
||||||
use serde::Serialize;
|
|
||||||
use tera::{Context, Error as TeraError, Tera};
|
|
||||||
use thiserror::Error;
|
|
||||||
use tracing::{debug, info};
|
|
||||||
use url::Url;
|
|
||||||
use warp::reject::Reject;
|
|
||||||
|
|
||||||
use crate::{errors::ErroredForm, filters::CsrfToken, storage::SessionInfo};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Templates(Arc<Tera>);
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum TemplateLoadingError {
|
|
||||||
#[error("could not load and compile some templates")]
|
|
||||||
Compile(#[from] TeraError),
|
|
||||||
|
|
||||||
#[error("missing templates {missing:?}")]
|
|
||||||
MissingTemplates {
|
|
||||||
missing: HashSet<String>,
|
|
||||||
loaded: HashSet<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Templates {
|
|
||||||
/// Load the templates and check all needed templates are properly loaded
|
|
||||||
pub fn load() -> Result<Self, TemplateLoadingError> {
|
|
||||||
let path = format!("{}/templates/**/*.{{html,txt}}", env!("CARGO_MANIFEST_DIR"));
|
|
||||||
info!(%path, "Loading templates");
|
|
||||||
let tera = Tera::new(&path)?;
|
|
||||||
|
|
||||||
let loaded: HashSet<_> = tera.get_template_names().collect();
|
|
||||||
let needed: HashSet<_> = std::array::IntoIter::new(TEMPLATES).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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum TemplateError {
|
|
||||||
#[error("could not prepare context for template {template:?}")]
|
|
||||||
Context {
|
|
||||||
template: &'static str,
|
|
||||||
#[source]
|
|
||||||
source: TeraError,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("could not render template {template:?}")]
|
|
||||||
Render {
|
|
||||||
template: &'static str,
|
|
||||||
#[source]
|
|
||||||
source: TeraError,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Reject for TemplateError {}
|
|
||||||
|
|
||||||
/// 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_rules! register_templates {
|
|
||||||
{
|
|
||||||
$(
|
|
||||||
// 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; count!( $( $template )* )] = [ $( $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 })
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
register_templates! {
|
|
||||||
/// Render the login page
|
|
||||||
pub fn render_login(WithCsrf<LoginContext>) { "login.html" }
|
|
||||||
|
|
||||||
/// Render the registration page
|
|
||||||
pub fn render_register(WithCsrf<()>) { "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<()>>) { "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" }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper trait to construct context wrappers
|
|
||||||
pub trait TemplateContext: Sized {
|
|
||||||
fn with_session(self, current_session: SessionInfo) -> WithSession<Self> {
|
|
||||||
WithSession {
|
|
||||||
current_session,
|
|
||||||
inner: self,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maybe_with_session(self, current_session: Option<SessionInfo>) -> WithOptionalSession<Self> {
|
|
||||||
WithOptionalSession {
|
|
||||||
current_session,
|
|
||||||
inner: self,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_csrf(self, token: &CsrfToken) -> WithCsrf<Self> {
|
|
||||||
WithCsrf {
|
|
||||||
csrf_token: token.form_value(),
|
|
||||||
inner: self,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Sized> TemplateContext for T {}
|
|
||||||
|
|
||||||
/// Context with a CSRF token in it
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct WithCsrf<T> {
|
|
||||||
csrf_token: String,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
inner: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Context with a user session in it
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct WithSession<T> {
|
|
||||||
current_session: SessionInfo,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
inner: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Context with an optional user session in it
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct WithOptionalSession<T> {
|
|
||||||
current_session: Option<SessionInfo>,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
inner: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub enum LoginFormField {
|
|
||||||
Username,
|
|
||||||
Password,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct LoginContext {
|
|
||||||
form: ErroredForm<LoginFormField>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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> FormPostContext<T> {
|
|
||||||
pub fn new(redirect_uri: Url, params: T) -> Self {
|
|
||||||
Self {
|
|
||||||
redirect_uri,
|
|
||||||
params,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Serialize)]
|
|
||||||
pub struct ErrorContext {
|
|
||||||
code: Option<&'static str>,
|
|
||||||
description: Option<String>,
|
|
||||||
details: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
177
crates/core/src/templates/context.rs
Normal file
177
crates/core/src/templates/context.rs
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use oauth2_types::errors::OAuth2Error;
|
||||||
|
use serde::Serialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{errors::ErroredForm, filters::CsrfToken, storage::SessionInfo};
|
||||||
|
|
||||||
|
/// Helper trait to construct context wrappers
|
||||||
|
pub trait TemplateContext: Sized {
|
||||||
|
fn with_session(self, current_session: SessionInfo) -> WithSession<Self> {
|
||||||
|
WithSession {
|
||||||
|
current_session,
|
||||||
|
inner: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_with_session(self, current_session: Option<SessionInfo>) -> WithOptionalSession<Self> {
|
||||||
|
WithOptionalSession {
|
||||||
|
current_session,
|
||||||
|
inner: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_csrf(self, token: &CsrfToken) -> WithCsrf<Self> {
|
||||||
|
WithCsrf {
|
||||||
|
csrf_token: token.form_value(),
|
||||||
|
inner: self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateContext for () {}
|
||||||
|
impl TemplateContext for IndexContext {}
|
||||||
|
impl TemplateContext for LoginContext {}
|
||||||
|
impl<T: Sized> TemplateContext for FormPostContext<T> {}
|
||||||
|
impl<T: Sized> TemplateContext for WithSession<T> {}
|
||||||
|
impl<T: Sized> TemplateContext for WithOptionalSession<T> {}
|
||||||
|
impl<T: Sized> TemplateContext for WithCsrf<T> {}
|
||||||
|
|
||||||
|
/// Context with a CSRF token in it
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct WithCsrf<T> {
|
||||||
|
csrf_token: String,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context with a user session in it
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct WithSession<T> {
|
||||||
|
current_session: SessionInfo,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context with an optional user session in it
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct WithOptionalSession<T> {
|
||||||
|
current_session: Option<SessionInfo>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
inner: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum LoginFormField {
|
||||||
|
Username,
|
||||||
|
Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct LoginContext {
|
||||||
|
form: ErroredForm<LoginFormField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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> FormPostContext<T> {
|
||||||
|
pub fn new(redirect_uri: Url, params: T) -> Self {
|
||||||
|
Self {
|
||||||
|
redirect_uri,
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize)]
|
||||||
|
pub struct ErrorContext {
|
||||||
|
code: Option<&'static str>,
|
||||||
|
description: Option<String>,
|
||||||
|
details: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
77
crates/core/src/templates/macros.rs
Normal file
77
crates/core/src/templates/macros.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// 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 })
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
179
crates/core/src/templates/mod.rs
Normal file
179
crates/core/src/templates/mod.rs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
use std::{collections::HashSet, io::Cursor, path::Path, string::ToString, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Context as _;
|
||||||
|
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;
|
||||||
|
|
||||||
|
mod context;
|
||||||
|
#[macro_use]
|
||||||
|
mod macros;
|
||||||
|
|
||||||
|
pub use self::context::{
|
||||||
|
ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField, TemplateContext,
|
||||||
|
WithCsrf, WithOptionalSession, WithSession,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Templates(Arc<Tera>);
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum TemplateLoadingError {
|
||||||
|
#[error("could not load and compile some templates")]
|
||||||
|
Compile(#[from] TeraError),
|
||||||
|
|
||||||
|
#[error("missing templates {missing:?}")]
|
||||||
|
MissingTemplates {
|
||||||
|
missing: HashSet<String>,
|
||||||
|
loaded: HashSet<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Templates {
|
||||||
|
/// 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<String>, 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum TemplateError {
|
||||||
|
#[error("could not prepare context for template {template:?}")]
|
||||||
|
Context {
|
||||||
|
template: &'static str,
|
||||||
|
#[source]
|
||||||
|
source: TeraError,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("could not render template {template:?}")]
|
||||||
|
Render {
|
||||||
|
template: &'static str,
|
||||||
|
#[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<()>) { "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<()>>) { "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" }
|
||||||
|
}
|
Reference in New Issue
Block a user