Strongly-typed templates
This commit is contained in:
@@ -17,14 +17,13 @@ use warp::{filters::BoxedFilter, reply::with_header, wrap_fn, Filter, Rejection,
|
||||
|
||||
use crate::{
|
||||
config::{CookiesConfig, CsrfConfig},
|
||||
errors::WrapError,
|
||||
filters::{
|
||||
csrf::{save_csrf_token, updated_csrf_token},
|
||||
session::with_optional_session,
|
||||
with_templates, CsrfToken,
|
||||
},
|
||||
storage::SessionInfo,
|
||||
templates::{CommonContext, Templates},
|
||||
templates::{TemplateContext, Templates},
|
||||
};
|
||||
|
||||
pub(super) fn filter(
|
||||
@@ -49,13 +48,9 @@ async fn get(
|
||||
csrf_token: CsrfToken,
|
||||
session: Option<SessionInfo>,
|
||||
) -> Result<(CsrfToken, impl Reply), Rejection> {
|
||||
let ctx = CommonContext::default()
|
||||
.with_csrf_token(&csrf_token)
|
||||
.maybe_with_session(session)
|
||||
.finish()
|
||||
.wrap_error()?;
|
||||
let ctx = ().maybe_with_session(session).with_csrf(&csrf_token);
|
||||
|
||||
let content = templates.render("index.html", &ctx).wrap_error()?;
|
||||
let content = templates.render_index(&ctx)?;
|
||||
Ok((
|
||||
csrf_token,
|
||||
with_header(content, "Content-Type", "text/html"),
|
||||
|
||||
@@ -23,11 +23,11 @@ use crate::{
|
||||
errors::WrapError,
|
||||
filters::{
|
||||
csrf::{protected_form, save_csrf_token, updated_csrf_token},
|
||||
session::{save_session, with_optional_session},
|
||||
session::save_session,
|
||||
with_pool, with_templates, CsrfToken,
|
||||
},
|
||||
storage::{login, SessionInfo},
|
||||
templates::{CommonContext, Templates},
|
||||
templates::{TemplateContext, Templates},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -45,7 +45,6 @@ pub(super) fn filter(
|
||||
let get = warp::get()
|
||||
.and(with_templates(templates))
|
||||
.and(updated_csrf_token(cookies_config, csrf_config))
|
||||
.and(with_optional_session(pool, cookies_config))
|
||||
.and_then(get)
|
||||
.untuple_one()
|
||||
.with(wrap_fn(save_csrf_token(cookies_config)));
|
||||
@@ -63,16 +62,11 @@ pub(super) fn filter(
|
||||
async fn get(
|
||||
templates: Templates,
|
||||
csrf_token: CsrfToken,
|
||||
session: Option<crate::storage::SessionInfo>,
|
||||
) -> Result<(CsrfToken, impl Reply), Rejection> {
|
||||
let ctx = CommonContext::default()
|
||||
.with_csrf_token(&csrf_token)
|
||||
.maybe_with_session(session)
|
||||
.finish()
|
||||
.wrap_error()?;
|
||||
let ctx = ().with_csrf(&csrf_token);
|
||||
|
||||
// TODO: check if there is an existing session
|
||||
let content = templates.render("login.html", &ctx).wrap_error()?;
|
||||
let content = templates.render_login(&ctx)?;
|
||||
Ok((
|
||||
csrf_token,
|
||||
with_header(content, "Content-Type", "text/html"),
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::{
|
||||
with_pool, with_templates, CsrfToken,
|
||||
},
|
||||
storage::SessionInfo,
|
||||
templates::{CommonContext, Templates},
|
||||
templates::{TemplateContext, Templates},
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -63,13 +63,9 @@ async fn get(
|
||||
csrf_token: CsrfToken,
|
||||
session: SessionInfo,
|
||||
) -> Result<(CsrfToken, impl Reply), Rejection> {
|
||||
let ctx = CommonContext::default()
|
||||
.with_csrf_token(&csrf_token)
|
||||
.with_session(session)
|
||||
.finish()
|
||||
.wrap_error()?;
|
||||
let ctx = ().with_session(session).with_csrf(&csrf_token);
|
||||
|
||||
let content = templates.render("reauth.html", &ctx).wrap_error()?;
|
||||
let content = templates.render_reauth(&ctx)?;
|
||||
Ok((
|
||||
csrf_token,
|
||||
with_header(content, "Content-Type", "text/html"),
|
||||
|
||||
@@ -12,62 +12,152 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
use std::{collections::HashSet, string::ToString, sync::Arc};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use serde::Serialize;
|
||||
use tera::{Context, Tera};
|
||||
use tracing::info;
|
||||
use tera::{Context, Error as TeraError, Tera};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, info};
|
||||
use warp::reject::Reject;
|
||||
|
||||
use crate::{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 {
|
||||
pub fn load() -> Result<Self, tera::Error> {
|
||||
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)?;
|
||||
Ok(Self(Arc::new(tera)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Templates {
|
||||
type Target = Tera;
|
||||
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();
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Default)]
|
||||
pub struct CommonContext {
|
||||
csrf_token: Option<String>,
|
||||
current_session: Option<SessionInfo>,
|
||||
}
|
||||
|
||||
impl CommonContext {
|
||||
pub fn with_csrf_token(self, token: &CsrfToken) -> Self {
|
||||
Self {
|
||||
csrf_token: Some(token.form_value()),
|
||||
..self
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_with_session(self, current_session: Option<SessionInfo>) -> Self {
|
||||
Self {
|
||||
#[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 {}
|
||||
|
||||
macro_rules! count {
|
||||
() => (0_usize);
|
||||
( $x:tt $($xs:tt)* ) => (1_usize + count!($($xs)*));
|
||||
}
|
||||
|
||||
macro_rules! register_templates {
|
||||
( $($(#[doc = $doc:expr])* $name:ident ($param:ty) => $template:expr),* $(,)? ) => {
|
||||
/// List of registered templates
|
||||
static TEMPLATES: [&'static str; count!($($template)*)] = [$($template),*];
|
||||
|
||||
impl Templates {
|
||||
$(
|
||||
$(#[doc = $doc])?
|
||||
pub fn $name(&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
|
||||
render_login(WithCsrf<()>) => "login.html",
|
||||
|
||||
/// Render the home page
|
||||
render_index(WithCsrf<WithOptionalSession<()>>) => "index.html",
|
||||
|
||||
/// Render the re-authentication form
|
||||
render_reauth(WithCsrf<WithSession<()>>) => "reauth.html",
|
||||
);
|
||||
|
||||
pub trait TemplateContext: Sized {
|
||||
fn with_session(self, current_session: SessionInfo) -> WithSession<Self> {
|
||||
WithSession {
|
||||
current_session,
|
||||
..self
|
||||
inner: self,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_session(self, current_session: SessionInfo) -> Self {
|
||||
self.maybe_with_session(Some(current_session))
|
||||
fn maybe_with_session(self, current_session: Option<SessionInfo>) -> WithOptionalSession<Self> {
|
||||
WithOptionalSession {
|
||||
current_session,
|
||||
inner: self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self) -> anyhow::Result<Context> {
|
||||
Context::from_serialize(&self).context("could not serialize common context for templates")
|
||||
fn with_csrf(self, token: &CsrfToken) -> WithCsrf<Self> {
|
||||
WithCsrf {
|
||||
csrf_token: token.form_value(),
|
||||
inner: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Sized> TemplateContext for T {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct WithCsrf<T> {
|
||||
csrf_token: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
inner: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct WithSession<T> {
|
||||
current_session: SessionInfo,
|
||||
|
||||
#[serde(flatten)]
|
||||
inner: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct WithOptionalSession<T> {
|
||||
current_session: Option<SessionInfo>,
|
||||
|
||||
#[serde(flatten)]
|
||||
inner: T,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user