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

@ -8,10 +8,6 @@ updates:
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/crates/static-files/"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/frontend/"
schedule:

52
Cargo.lock generated
View File

@ -2516,7 +2516,6 @@ dependencies = [
"mas-policy",
"mas-router",
"mas-spa",
"mas-static-files",
"mas-storage",
"mas-tasks",
"mas-templates",
@ -2636,6 +2635,7 @@ dependencies = [
"axum 0.6.0-rc.4",
"axum-extra",
"axum-macros",
"camino",
"chrono",
"futures-util",
"headers",
@ -2860,22 +2860,6 @@ dependencies = [
"tower-service",
]
[[package]]
name = "mas-static-files"
version = "0.1.0"
dependencies = [
"axum 0.6.0-rc.4",
"camino",
"headers",
"http",
"http-body",
"mime_guess",
"rust-embed",
"tower",
"tower-http",
"tracing",
]
[[package]]
name = "mas-storage"
version = "0.1.0"
@ -4110,40 +4094,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rust-embed"
version = "6.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "6.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "7.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054"
dependencies = [
"sha2 0.10.6",
"walkdir",
]
[[package]]
name = "rustc-demangle"
version = "0.1.21"

View File

@ -17,23 +17,6 @@ ARG ZIG_VERSION=0.9.1
ARG NODEJS_VERSION=18
ARG OPA_VERSION=0.45.0
##############################################
## Build stage that builds the static files ##
##############################################
FROM --platform=${BUILDPLATFORM} docker.io/library/node:${NODEJS_VERSION}-${DEBIAN_VERSION_NAME}-slim AS static-files
WORKDIR /app/crates/static-files
COPY ./crates/static-files/package.json ./crates/static-files/package-lock.json /app/crates/static-files/
RUN npm ci
COPY . /app/
RUN npm run build
# Change the timestamp of built files for better caching
RUN find public -type f -exec touch -t 197001010000.00 {} +
##########################################
## Build stage that builds the frontend ##
##########################################
@ -50,13 +33,10 @@ RUN npm run build
# Move the built files
RUN \
mkdir -p /usr/local/share/mas-cli/frontend-assets && \
cp ./dist/manifest.json /usr/local/share/mas-cli/frontend-manifest.json && \
mkdir -p /share/assets && \
cp ./dist/manifest.json /share/manifest.json && \
rm -f ./dist/index.html* ./dist/manifest.json* && \
cp ./dist/* /usr/local/share/mas-cli/frontend-assets/
# Change the timestamp of built files for better caching
RUN find /usr/local/share/mas-cli -exec touch -t 197001010000.00 {} +
cp ./dist/* /share/assets/
##############################################
## Build stage that builds the OPA policies ##
@ -147,7 +127,6 @@ RUN cargo chef cook \
# Build the rest
COPY ./Cargo.toml ./Cargo.lock /app/
COPY ./crates /app/crates
COPY --from=static-files /app/crates/static-files/public /app/crates/static-files/public
ENV SQLX_OFFLINE=true
RUN cargo auditable zigbuild \
--locked \
@ -160,14 +139,22 @@ RUN cargo auditable zigbuild \
# Move the binary to avoid having to guess its name in the next stage
RUN mv target/$(/docker-arch-to-rust-target.sh "${TARGETPLATFORM}")/release/mas-cli /usr/local/bin/mas-cli
#######################################
## Prepare /usr/local/share/mas-cli/ ##
#######################################
FROM --platform=${BUILDPLATFORM} scratch AS share
COPY --from=frontend /share /share
COPY --from=policy /app/policies/policy.wasm /share/policy.wasm
COPY ./templates/ /share/templates
##################################
## Runtime stage, debug variant ##
##################################
FROM --platform=${TARGETPLATFORM} gcr.io/distroless/cc-debian${DEBIAN_VERSION}:debug-nonroot AS debug
COPY --from=builder /usr/local/bin/mas-cli /usr/local/bin/mas-cli
COPY --from=frontend /usr/local/share/mas-cli /usr/local/share/mas-cli
COPY --from=policy /app/policies/policy.wasm /usr/local/share/mas-cli/policy.wasm
COPY --from=share /share /usr/local/share/mas-cli
WORKDIR /
ENTRYPOINT ["/usr/local/bin/mas-cli"]
@ -178,8 +165,7 @@ ENTRYPOINT ["/usr/local/bin/mas-cli"]
FROM --platform=${TARGETPLATFORM} gcr.io/distroless/cc-debian${DEBIAN_VERSION}:nonroot
COPY --from=builder /usr/local/bin/mas-cli /usr/local/bin/mas-cli
COPY --from=frontend /usr/local/share/mas-cli /usr/local/share/mas-cli
COPY --from=policy /app/policies/policy.wasm /usr/local/share/mas-cli/policy.wasm
COPY --from=share /share /usr/local/share/mas-cli
WORKDIR /
ENTRYPOINT ["/usr/local/bin/mas-cli"]

View File

@ -11,13 +11,6 @@ See the [Documentation](https://matrix-org.github.io/matrix-authentication-servi
- [Install Node.js and npm](https://nodejs.org/)
- [Install Open Policy Agent](https://www.openpolicyagent.org/docs/latest/#1-download-opa)
- Clone this repository
- Generate the static-files:
```sh
cd crates/static-files
npm ci
npm run build
cd ../..
```
- Build the frontend
```sh
cd frontend

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

@ -85,10 +85,10 @@
"playground": true
},
{
"name": "static"
"name": "assets",
"path": "./frontend/dist/"
},
{
"assets": "./frontend/dist/",
"manifest": "./frontend/dist/manifest.json",
"name": "spa"
}
@ -171,8 +171,7 @@
"templates": {
"description": "Configuration related to templates",
"default": {
"builtin": true,
"path": null
"path": "./templates/"
},
"allOf": [
{
@ -1402,11 +1401,12 @@
"name": {
"type": "string",
"enum": [
"static"
"assets"
]
},
"web_root": {
"description": "Path from which to serve static files. If not specified, it will serve the static files embedded in the server binary",
"path": {
"description": "Path to the directory to serve.",
"default": "./frontend/dist/",
"type": "string"
}
}
@ -1433,13 +1433,8 @@
"name"
],
"properties": {
"assets": {
"description": "Path to the assets to server",
"default": "./frontend/dist/",
"type": "string"
},
"manifest": {
"description": "Path to the vite mamanifest.jsonnifest",
"description": "Path to the vite manifest.json",
"default": "./frontend/dist/manifest.json",
"type": "string"
},
@ -1511,14 +1506,9 @@
"description": "Configuration related to templates",
"type": "object",
"properties": {
"builtin": {
"description": "Load the templates embedded in the binary",
"default": true,
"type": "boolean"
},
"path": {
"description": "Path to the folder that holds the custom templates",
"default": null,
"description": "Path to the folder which holds the templates",
"default": "./templates/",
"type": "string"
}
}

View File

@ -8,7 +8,8 @@
"generate": "relay-compiler && eslint --fix .",
"lint": "relay-compiler --validate && eslint . && tsc",
"relay": "relay-compiler",
"build": "npm run lint && vite build --base=./",
"build": "npm run lint && vite build --base=./ && npm run build:templates",
"build:templates": "tailwindcss --postcss --minify --config ./tailwind.templates.config.cjs -o dist/tailwind.css",
"preview": "vite preview",
"test": "jest",
"storybook": "storybook dev -p 6006",

View File

@ -1,4 +1,4 @@
// Copyright 2021 The Matrix.org Foundation C.I.C.
// 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.
@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
const base = require("./tailwind.config.cjs");
/** @type {import('tailwindcss').Config} */
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
cssnano: {},
}
}
...base,
content: ["../crates/templates/res/**/*.html"],
darkMode: "media",
};