You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
Do not embed the templates and static files in the binary
This commit is contained in:
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@ -8,10 +8,6 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/crates/static-files/"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/frontend/"
|
directory: "/frontend/"
|
||||||
schedule:
|
schedule:
|
||||||
|
52
Cargo.lock
generated
52
Cargo.lock
generated
@ -2516,7 +2516,6 @@ dependencies = [
|
|||||||
"mas-policy",
|
"mas-policy",
|
||||||
"mas-router",
|
"mas-router",
|
||||||
"mas-spa",
|
"mas-spa",
|
||||||
"mas-static-files",
|
|
||||||
"mas-storage",
|
"mas-storage",
|
||||||
"mas-tasks",
|
"mas-tasks",
|
||||||
"mas-templates",
|
"mas-templates",
|
||||||
@ -2636,6 +2635,7 @@ dependencies = [
|
|||||||
"axum 0.6.0-rc.4",
|
"axum 0.6.0-rc.4",
|
||||||
"axum-extra",
|
"axum-extra",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
|
"camino",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"headers",
|
"headers",
|
||||||
@ -2860,22 +2860,6 @@ dependencies = [
|
|||||||
"tower-service",
|
"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]]
|
[[package]]
|
||||||
name = "mas-storage"
|
name = "mas-storage"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -4110,40 +4094,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.21"
|
version = "0.1.21"
|
||||||
|
42
Dockerfile
42
Dockerfile
@ -17,23 +17,6 @@ ARG ZIG_VERSION=0.9.1
|
|||||||
ARG NODEJS_VERSION=18
|
ARG NODEJS_VERSION=18
|
||||||
ARG OPA_VERSION=0.45.0
|
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 ##
|
## Build stage that builds the frontend ##
|
||||||
##########################################
|
##########################################
|
||||||
@ -50,13 +33,10 @@ RUN npm run build
|
|||||||
|
|
||||||
# Move the built files
|
# Move the built files
|
||||||
RUN \
|
RUN \
|
||||||
mkdir -p /usr/local/share/mas-cli/frontend-assets && \
|
mkdir -p /share/assets && \
|
||||||
cp ./dist/manifest.json /usr/local/share/mas-cli/frontend-manifest.json && \
|
cp ./dist/manifest.json /share/manifest.json && \
|
||||||
rm -f ./dist/index.html* ./dist/manifest.json* && \
|
rm -f ./dist/index.html* ./dist/manifest.json* && \
|
||||||
cp ./dist/* /usr/local/share/mas-cli/frontend-assets/
|
cp ./dist/* /share/assets/
|
||||||
|
|
||||||
# Change the timestamp of built files for better caching
|
|
||||||
RUN find /usr/local/share/mas-cli -exec touch -t 197001010000.00 {} +
|
|
||||||
|
|
||||||
##############################################
|
##############################################
|
||||||
## Build stage that builds the OPA policies ##
|
## Build stage that builds the OPA policies ##
|
||||||
@ -147,7 +127,6 @@ RUN cargo chef cook \
|
|||||||
# Build the rest
|
# Build the rest
|
||||||
COPY ./Cargo.toml ./Cargo.lock /app/
|
COPY ./Cargo.toml ./Cargo.lock /app/
|
||||||
COPY ./crates /app/crates
|
COPY ./crates /app/crates
|
||||||
COPY --from=static-files /app/crates/static-files/public /app/crates/static-files/public
|
|
||||||
ENV SQLX_OFFLINE=true
|
ENV SQLX_OFFLINE=true
|
||||||
RUN cargo auditable zigbuild \
|
RUN cargo auditable zigbuild \
|
||||||
--locked \
|
--locked \
|
||||||
@ -160,14 +139,22 @@ RUN cargo auditable zigbuild \
|
|||||||
# Move the binary to avoid having to guess its name in the next stage
|
# 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
|
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 ##
|
## Runtime stage, debug variant ##
|
||||||
##################################
|
##################################
|
||||||
FROM --platform=${TARGETPLATFORM} gcr.io/distroless/cc-debian${DEBIAN_VERSION}:debug-nonroot AS debug
|
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=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=share /share /usr/local/share/mas-cli
|
||||||
COPY --from=policy /app/policies/policy.wasm /usr/local/share/mas-cli/policy.wasm
|
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
ENTRYPOINT ["/usr/local/bin/mas-cli"]
|
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
|
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=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=share /share /usr/local/share/mas-cli
|
||||||
COPY --from=policy /app/policies/policy.wasm /usr/local/share/mas-cli/policy.wasm
|
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
ENTRYPOINT ["/usr/local/bin/mas-cli"]
|
ENTRYPOINT ["/usr/local/bin/mas-cli"]
|
||||||
|
@ -11,13 +11,6 @@ See the [Documentation](https://matrix-org.github.io/matrix-authentication-servi
|
|||||||
- [Install Node.js and npm](https://nodejs.org/)
|
- [Install Node.js and npm](https://nodejs.org/)
|
||||||
- [Install Open Policy Agent](https://www.openpolicyagent.org/docs/latest/#1-download-opa)
|
- [Install Open Policy Agent](https://www.openpolicyagent.org/docs/latest/#1-download-opa)
|
||||||
- Clone this repository
|
- Clone this repository
|
||||||
- Generate the static-files:
|
|
||||||
```sh
|
|
||||||
cd crates/static-files
|
|
||||||
npm ci
|
|
||||||
npm run build
|
|
||||||
cd ../..
|
|
||||||
```
|
|
||||||
- Build the frontend
|
- Build the frontend
|
||||||
```sh
|
```sh
|
||||||
cd frontend
|
cd frontend
|
||||||
|
@ -49,7 +49,6 @@ mas-listener = { path = "../listener" }
|
|||||||
mas-policy = { path = "../policy" }
|
mas-policy = { path = "../policy" }
|
||||||
mas-router = { path = "../router" }
|
mas-router = { path = "../router" }
|
||||||
mas-spa = { path = "../spa" }
|
mas-spa = { path = "../spa" }
|
||||||
mas-static-files = { path = "../static-files" }
|
|
||||||
mas-storage = { path = "../storage" }
|
mas-storage = { path = "../storage" }
|
||||||
mas-tasks = { path = "../tasks" }
|
mas-tasks = { path = "../tasks" }
|
||||||
mas-templates = { path = "../templates" }
|
mas-templates = { path = "../templates" }
|
||||||
@ -71,9 +70,6 @@ native-roots = ["mas-http/native-roots", "mas-handlers/native-roots"]
|
|||||||
# Use the webpki root certificates
|
# Use the webpki root certificates
|
||||||
webpki-roots = ["mas-http/webpki-roots", "mas-handlers/webpki-roots"]
|
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.
|
# Enable OpenTelemetry OTLP exporter.
|
||||||
otlp = ["dep:opentelemetry-otlp"]
|
otlp = ["dep:opentelemetry-otlp"]
|
||||||
# Enable OpenTelemetry Jaeger exporter and propagator.
|
# Enable OpenTelemetry Jaeger exporter and propagator.
|
||||||
|
@ -55,44 +55,36 @@ async fn watch_templates(
|
|||||||
|
|
||||||
let templates = templates.clone();
|
let templates = templates.clone();
|
||||||
|
|
||||||
// Find which roots we're supposed to watch
|
// Find which root we're supposed to watch
|
||||||
let roots = templates.watch_roots().await;
|
let root = templates.watch_root();
|
||||||
let mut streams = Vec::new();
|
|
||||||
|
|
||||||
for root in roots {
|
// For each root, create a subscription
|
||||||
// For each root, create a subscription
|
let resolved = client
|
||||||
let resolved = client
|
.resolve_root(CanonicalPath::canonicalize(root)?)
|
||||||
.resolve_root(CanonicalPath::canonicalize(root)?)
|
.await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO: we could subscribe to less, properly filter here
|
// TODO: we could subscribe to less, properly filter here
|
||||||
let (subscription, _) = client
|
let (subscription, _) = client
|
||||||
.subscribe::<NameOnly>(&resolved, SubscribeRequest::default())
|
.subscribe::<NameOnly>(&resolved, SubscribeRequest::default())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Create a stream out of that subscription
|
// Create a stream out of that subscription
|
||||||
let stream = futures_util::stream::try_unfold(subscription, |mut sub| async move {
|
let fut = futures_util::stream::try_unfold(subscription, |mut sub| async move {
|
||||||
let next = sub.next().await?;
|
let next = sub.next().await?;
|
||||||
anyhow::Ok(Some((next, sub)))
|
anyhow::Ok(Some((next, sub)))
|
||||||
});
|
})
|
||||||
|
.try_filter_map(|event| async move {
|
||||||
streams.push(Box::pin(stream));
|
match event {
|
||||||
}
|
SubscriptionData::FilesChanged(QueryResult {
|
||||||
|
files: Some(files), ..
|
||||||
let files_changed_stream =
|
}) => {
|
||||||
futures_util::stream::select_all(streams).try_filter_map(|event| async move {
|
let files: Vec<_> = files.into_iter().map(|f| f.name.into_inner()).collect();
|
||||||
match event {
|
Ok(Some(files))
|
||||||
SubscriptionData::FilesChanged(QueryResult {
|
|
||||||
files: Some(files), ..
|
|
||||||
}) => {
|
|
||||||
let files: Vec<_> = files.into_iter().map(|f| f.name.into_inner()).collect();
|
|
||||||
Ok(Some(files))
|
|
||||||
}
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
}
|
||||||
});
|
_ => Ok(None),
|
||||||
|
}
|
||||||
let fut = files_changed_stream.for_each(move |files| {
|
})
|
||||||
|
.for_each(move |files| {
|
||||||
let templates = templates.clone();
|
let templates = templates.clone();
|
||||||
async move {
|
async move {
|
||||||
info!(?files, "Files changed, reloading templates");
|
info!(?files, "Files changed, reloading templates");
|
||||||
@ -162,13 +154,9 @@ impl Options {
|
|||||||
let url_builder = UrlBuilder::new(config.http.public_base.clone());
|
let url_builder = UrlBuilder::new(config.http.public_base.clone());
|
||||||
|
|
||||||
// Load and compile the templates
|
// Load and compile the templates
|
||||||
let templates = Templates::load(
|
let templates = Templates::load(config.templates.path.clone(), url_builder.clone())
|
||||||
config.templates.path.clone(),
|
.await
|
||||||
config.templates.builtin,
|
.context("could not load templates")?;
|
||||||
url_builder.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.context("could not load templates")?;
|
|
||||||
|
|
||||||
let mailer = Mailer::new(
|
let mailer = Mailer::new(
|
||||||
&templates,
|
&templates,
|
||||||
|
@ -25,24 +25,10 @@ pub(super) struct Options {
|
|||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
enum Subcommand {
|
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 for template validity at given path.
|
||||||
Check {
|
Check {
|
||||||
/// Path where the templates are
|
/// Path where the templates are
|
||||||
path: String,
|
path: Utf8PathBuf,
|
||||||
|
|
||||||
/// Skip loading builtin templates
|
|
||||||
#[arg(long)]
|
|
||||||
skip_builtin: bool,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,17 +36,10 @@ impl Options {
|
|||||||
pub async fn run(&self, _root: &super::Options) -> anyhow::Result<()> {
|
pub async fn run(&self, _root: &super::Options) -> anyhow::Result<()> {
|
||||||
use Subcommand as SC;
|
use Subcommand as SC;
|
||||||
match &self.subcommand {
|
match &self.subcommand {
|
||||||
SC::Save { path, overwrite } => {
|
SC::Check { path } => {
|
||||||
Templates::save(path, *overwrite).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
SC::Check { path, skip_builtin } => {
|
|
||||||
let clock = Clock::default();
|
let clock = Clock::default();
|
||||||
let url_builder = mas_router::UrlBuilder::new("https://example.com/".parse()?);
|
let url_builder = mas_router::UrlBuilder::new("https://example.com/".parse()?);
|
||||||
let templates =
|
let templates = Templates::load(path.clone(), url_builder).await?;
|
||||||
Templates::load(Some(path.into()), !skip_builtin, url_builder).await?;
|
|
||||||
templates.check_render(clock.now()).await?;
|
templates.check_render(clock.now()).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -59,9 +59,15 @@ where
|
|||||||
mas_config::HttpResource::GraphQL { playground } => {
|
mas_config::HttpResource::GraphQL { playground } => {
|
||||||
router.merge(mas_handlers::graphql_router::<AppState, B>(*playground))
|
router.merge(mas_handlers::graphql_router::<AppState, B>(*playground))
|
||||||
}
|
}
|
||||||
mas_config::HttpResource::Static { web_root } => {
|
mas_config::HttpResource::Assets { path } => {
|
||||||
let handler = mas_static_files::service(web_root.as_deref());
|
let static_service = ServeDir::new(path).append_index_html_on_directories(false);
|
||||||
router.nest_service(mas_router::StaticAsset::route(), handler)
|
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 => {
|
mas_config::HttpResource::OAuth => {
|
||||||
router.merge(mas_handlers::api_router::<AppState, B>())
|
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 =
|
let error_layer =
|
||||||
HandleErrorLayer::new(|_e| ready(StatusCode::INTERNAL_SERVER_ERROR));
|
HandleErrorLayer::new(|_e| ready(StatusCode::INTERNAL_SERVER_ERROR));
|
||||||
|
|
||||||
// TODO: split the assets service and the index service, and make those paths
|
// TODO: make those paths configurable
|
||||||
// configurable
|
|
||||||
let assets_base = "/app-assets/";
|
|
||||||
let app_base = "/app/";
|
let app_base = "/app/";
|
||||||
|
|
||||||
// TODO: make that config typed and configurable
|
// TODO: make that config typed and configurable
|
||||||
@ -91,14 +95,13 @@ where
|
|||||||
"root": app_base,
|
"root": app_base,
|
||||||
});
|
});
|
||||||
|
|
||||||
let index_service =
|
let index_service = ViteManifestService::new(
|
||||||
ViteManifestService::new(manifest.clone(), assets_base.into(), config);
|
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))
|
||||||
|
|
||||||
router
|
|
||||||
.nest_service(app_base, error_layer.layer(index_service))
|
|
||||||
.nest_service(assets_base, error_layer.layer(static_service))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,18 +49,18 @@ fn http_listener_spa_manifest_default() -> Utf8PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "docker"))]
|
#[cfg(not(feature = "docker"))]
|
||||||
fn http_listener_spa_assets_default() -> Utf8PathBuf {
|
fn http_listener_assets_path_default() -> Utf8PathBuf {
|
||||||
"./frontend/dist/".into()
|
"./frontend/dist/".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "docker")]
|
#[cfg(feature = "docker")]
|
||||||
fn http_listener_spa_manifest_default() -> Utf8PathBuf {
|
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")]
|
#[cfg(feature = "docker")]
|
||||||
fn http_listener_spa_assets_default() -> Utf8PathBuf {
|
fn http_listener_assets_path_default() -> Utf8PathBuf {
|
||||||
"/usr/local/share/mas-cli/frontend-assets/".into()
|
"/usr/local/share/mas-cli/assets/".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kind of socket
|
/// Kind of socket
|
||||||
@ -272,12 +272,11 @@ pub enum Resource {
|
|||||||
Compat,
|
Compat,
|
||||||
|
|
||||||
/// Static files
|
/// Static files
|
||||||
Static {
|
Assets {
|
||||||
/// Path from which to serve static files. If not specified, it will
|
/// Path to the directory to serve.
|
||||||
/// serve the static files embedded in the server binary
|
#[serde(default = "http_listener_assets_path_default")]
|
||||||
#[serde(default)]
|
#[schemars(with = "String")]
|
||||||
#[schemars(with = "Option<String>")]
|
path: Utf8PathBuf,
|
||||||
web_root: Option<Utf8PathBuf>,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Mount a "/connection-info" handler which helps debugging informations on
|
/// Mount a "/connection-info" handler which helps debugging informations on
|
||||||
@ -287,15 +286,10 @@ pub enum Resource {
|
|||||||
|
|
||||||
/// Mount the single page app
|
/// Mount the single page app
|
||||||
Spa {
|
Spa {
|
||||||
/// Path to the vite mamanifest.jsonnifest
|
/// Path to the vite manifest.json
|
||||||
#[serde(default = "http_listener_spa_manifest_default")]
|
#[serde(default = "http_listener_spa_manifest_default")]
|
||||||
#[schemars(with = "String")]
|
#[schemars(with = "String")]
|
||||||
manifest: Utf8PathBuf,
|
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::OAuth,
|
||||||
Resource::Compat,
|
Resource::Compat,
|
||||||
Resource::GraphQL { playground: true },
|
Resource::GraphQL { playground: true },
|
||||||
Resource::Static { web_root: None },
|
Resource::Assets {
|
||||||
|
path: http_listener_assets_path_default(),
|
||||||
|
},
|
||||||
Resource::Spa {
|
Resource::Spa {
|
||||||
manifest: http_listener_spa_manifest_default(),
|
manifest: http_listener_spa_manifest_default(),
|
||||||
assets: http_listener_spa_assets_default(),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
tls: None,
|
tls: None,
|
||||||
|
@ -20,28 +20,29 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use super::ConfigurationSection;
|
use super::ConfigurationSection;
|
||||||
|
|
||||||
fn default_builtin() -> bool {
|
#[cfg(not(feature = "docker"))]
|
||||||
true
|
fn default_path() -> Utf8PathBuf {
|
||||||
|
"./templates/".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "docker")]
|
||||||
|
fn default_path() -> Utf8PathBuf {
|
||||||
|
"/usr/local/share/mas-cli/templates/".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration related to templates
|
/// Configuration related to templates
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
|
||||||
pub struct TemplatesConfig {
|
pub struct TemplatesConfig {
|
||||||
/// Path to the folder that holds the custom templates
|
/// Path to the folder which holds the templates
|
||||||
#[serde(default)]
|
#[serde(default = "default_path")]
|
||||||
#[schemars(with = "Option<String>")]
|
#[schemars(with = "Option<String>")]
|
||||||
pub path: Option<Utf8PathBuf>,
|
pub path: Utf8PathBuf,
|
||||||
|
|
||||||
/// Load the templates embedded in the binary
|
|
||||||
#[serde(default = "default_builtin")]
|
|
||||||
pub builtin: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TemplatesConfig {
|
impl Default for TemplatesConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: None,
|
path: default_path(),
|
||||||
builtin: default_builtin(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ serde_urlencoded = "0.7.1"
|
|||||||
argon2 = { version = "0.4.1", features = ["password-hash"] }
|
argon2 = { version = "0.4.1", features = ["password-hash"] }
|
||||||
|
|
||||||
# Various data types and utilities
|
# Various data types and utilities
|
||||||
|
camino = "1.1.1"
|
||||||
chrono = { version = "0.4.23", features = ["serde"] }
|
chrono = { version = "0.4.23", features = ["serde"] }
|
||||||
url = { version = "2.3.1", features = ["serde"] }
|
url = { version = "2.3.1", features = ["serde"] }
|
||||||
mime = "0.3.16"
|
mime = "0.3.16"
|
||||||
|
@ -362,9 +362,13 @@ where
|
|||||||
async fn test_state(pool: PgPool) -> Result<AppState, anyhow::Error> {
|
async fn test_state(pool: PgPool) -> Result<AppState, anyhow::Error> {
|
||||||
use mas_email::MailTransport;
|
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 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
|
// TODO: add test keys to the store
|
||||||
let key_store = Keystore::default();
|
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());
|
let homeserver = MatrixHomeserver::new("example.com".to_owned());
|
||||||
|
|
||||||
#[allow(clippy::disallowed_types)]
|
let file = tokio::fs::File::open(workspace_root.join("policies").join("policy.wasm")).await?;
|
||||||
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 policy_factory = PolicyFactory::load(
|
let policy_factory = PolicyFactory::load(
|
||||||
file,
|
file,
|
||||||
|
@ -411,8 +411,7 @@ mod tests {
|
|||||||
fn timestamp_serde() {
|
fn timestamp_serde() {
|
||||||
let datetime = Timestamp(
|
let datetime = Timestamp(
|
||||||
chrono::Utc
|
chrono::Utc
|
||||||
.ymd_opt(2018, 1, 18)
|
.with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
|
||||||
.and_hms_opt(1, 30, 22)
|
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
let timestamp = serde_json::Value::Number(1_516_239_022.into());
|
let timestamp = serde_json::Value::Number(1_516_239_022.into());
|
||||||
@ -451,8 +450,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn extract_claims() {
|
fn extract_claims() {
|
||||||
let now = chrono::Utc
|
let now = chrono::Utc
|
||||||
.ymd_opt(2018, 1, 18)
|
.with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
|
||||||
.and_hms_opt(1, 30, 22)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let expiration = now + chrono::Duration::minutes(5);
|
let expiration = now + chrono::Duration::minutes(5);
|
||||||
let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
|
let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
|
||||||
@ -496,8 +494,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn time_validation() {
|
fn time_validation() {
|
||||||
let now = chrono::Utc
|
let now = chrono::Utc
|
||||||
.ymd_opt(2018, 1, 18)
|
.with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
|
||||||
.and_hms_opt(1, 30, 22)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let claims = serde_json::json!({
|
let claims = serde_json::json!({
|
||||||
@ -604,8 +601,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn invalid_claims() {
|
fn invalid_claims() {
|
||||||
let now = chrono::Utc
|
let now = chrono::Utc
|
||||||
.ymd_opt(2018, 1, 18)
|
.with_ymd_and_hms(2018, 1, 18, 1, 30, 22)
|
||||||
.and_hms_opt(1, 30, 22)
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
|
let time_options = TimeOptions::new(now).leeway(chrono::Duration::zero());
|
||||||
|
|
||||||
|
@ -539,7 +539,7 @@ impl StaticAsset {
|
|||||||
impl Route for StaticAsset {
|
impl Route for StaticAsset {
|
||||||
type Query = ();
|
type Query = ();
|
||||||
fn route() -> &'static str {
|
fn route() -> &'static str {
|
||||||
"/assets"
|
"/assets/"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn path(&self) -> std::borrow::Cow<'static, str> {
|
fn path(&self) -> std::borrow::Cow<'static, str> {
|
||||||
|
2
crates/static-files/.gitignore
vendored
2
crates/static-files/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
/node_modules/
|
|
||||||
/public/tailwind.css
|
|
@ -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"
|
|
2832
crates/static-files/package-lock.json
generated
2832
crates/static-files/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
ok
|
|
@ -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()
|
|
||||||
}
|
|
@ -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'),
|
|
||||||
],
|
|
||||||
}
|
|
@ -5,9 +5,6 @@ authors = ["Quentin Gliech <quenting@element.io>"]
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
[features]
|
|
||||||
dev = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tokio = { version = "1.21.2", features = ["macros"] }
|
tokio = { version = "1.21.2", features = ["macros"] }
|
||||||
|
@ -24,16 +24,16 @@
|
|||||||
|
|
||||||
//! Templates rendering
|
//! 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 camino::{Utf8Path, Utf8PathBuf};
|
||||||
use mas_data_model::StorageBackend;
|
use mas_data_model::StorageBackend;
|
||||||
use mas_router::UrlBuilder;
|
use mas_router::UrlBuilder;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tera::{Context, Error as TeraError, Tera};
|
use tera::{Context, Error as TeraError, Tera};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::{fs::OpenOptions, io::AsyncWriteExt, sync::RwLock, task::JoinError};
|
use tokio::{sync::RwLock, task::JoinError};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
mod context;
|
mod context;
|
||||||
@ -59,13 +59,16 @@ pub use self::{
|
|||||||
pub struct Templates {
|
pub struct Templates {
|
||||||
tera: Arc<RwLock<Tera>>,
|
tera: Arc<RwLock<Tera>>,
|
||||||
url_builder: UrlBuilder,
|
url_builder: UrlBuilder,
|
||||||
path: Option<Utf8PathBuf>,
|
path: Utf8PathBuf,
|
||||||
builtin: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// There was an issue while loading the templates
|
/// There was an issue while loading the templates
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum TemplateLoadingError {
|
pub enum TemplateLoadingError {
|
||||||
|
/// I/O error
|
||||||
|
#[error(transparent)]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
|
|
||||||
/// Some templates failed to compile
|
/// Some templates failed to compile
|
||||||
#[error("could not load and compile some templates")]
|
#[error("could not load and compile some templates")]
|
||||||
Compile(#[from] TeraError),
|
Compile(#[from] TeraError),
|
||||||
@ -85,116 +88,42 @@ pub enum TemplateLoadingError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Templates {
|
impl Templates {
|
||||||
/// List directories to watch
|
/// Directories to watch
|
||||||
pub async fn watch_roots(&self) -> Vec<Utf8PathBuf> {
|
#[must_use]
|
||||||
Self::roots(self.path.as_deref(), self.builtin)
|
pub fn watch_root(&self) -> &Utf8Path {
|
||||||
.await
|
&self.path
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the templates from the given config
|
/// Load the templates from the given config
|
||||||
pub async fn load(
|
pub async fn load(
|
||||||
path: Option<Utf8PathBuf>,
|
path: Utf8PathBuf,
|
||||||
builtin: bool,
|
|
||||||
url_builder: UrlBuilder,
|
url_builder: UrlBuilder,
|
||||||
) -> Result<Self, TemplateLoadingError> {
|
) -> 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 {
|
Ok(Self {
|
||||||
tera: Arc::new(RwLock::new(tera)),
|
tera: Arc::new(RwLock::new(tera)),
|
||||||
path,
|
path,
|
||||||
url_builder,
|
url_builder,
|
||||||
builtin,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_(
|
async fn load_(path: &Utf8Path, url_builder: UrlBuilder) -> Result<Tera, TemplateLoadingError> {
|
||||||
path: Option<&Utf8Path>,
|
let path = path.to_owned();
|
||||||
builtin: bool,
|
|
||||||
url_builder: UrlBuilder,
|
|
||||||
) -> Result<Tera, TemplateLoadingError> {
|
|
||||||
let mut teras = Vec::new();
|
|
||||||
|
|
||||||
let roots = Self::roots(path, builtin).await;
|
// This uses blocking I/Os, do that in a blocking task
|
||||||
for maybe_root in roots {
|
let mut tera = tokio::task::spawn_blocking(move || {
|
||||||
let root = match maybe_root {
|
let path = path.canonicalize_utf8()?;
|
||||||
Ok(root) => root,
|
let path = format!("{}/**/*.{{html,txt,subject}}", path);
|
||||||
Err(err) => {
|
|
||||||
warn!(%err, "Could not open a template folder, skipping it");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// This uses blocking I/Os, do that in a blocking task
|
info!(%path, "Loading templates from filesystem");
|
||||||
let tera = tokio::task::spawn_blocking(move || {
|
Tera::new(&path)
|
||||||
let path = format!("{}/**/*.{{html,txt,subject}}", root);
|
})
|
||||||
info!(%path, "Loading templates from filesystem");
|
.await??;
|
||||||
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()?;
|
|
||||||
|
|
||||||
self::functions::register(&mut tera, url_builder);
|
self::functions::register(&mut tera, url_builder);
|
||||||
|
|
||||||
let loaded: HashSet<_> = tera.get_template_names().collect();
|
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");
|
debug!(?loaded, ?needed, "Templates loaded");
|
||||||
let missing: HashSet<_> = needed.difference(&loaded).collect();
|
let missing: HashSet<_> = needed.difference(&loaded).collect();
|
||||||
|
|
||||||
@ -210,61 +139,13 @@ impl Templates {
|
|||||||
/// Reload the templates on disk
|
/// Reload the templates on disk
|
||||||
pub async fn reload(&self) -> anyhow::Result<()> {
|
pub async fn reload(&self) -> anyhow::Result<()> {
|
||||||
// Prepare the new Tera instance
|
// Prepare the new Tera instance
|
||||||
let new_tera =
|
let new_tera = Self::load_(&self.path, self.url_builder.clone()).await?;
|
||||||
Self::load_(self.path.as_deref(), self.builtin, self.url_builder.clone()).await?;
|
|
||||||
|
|
||||||
// Swap it
|
// Swap it
|
||||||
*self.tera.write().await = new_tera;
|
*self.tera.write().await = new_tera;
|
||||||
|
|
||||||
Ok(())
|
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
|
/// Failed to render a template
|
||||||
@ -294,16 +175,6 @@ pub enum TemplateError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
register_templates! {
|
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
|
/// Render the login page
|
||||||
pub fn render_login(WithCsrf<LoginContext>) { "pages/login.html" }
|
pub fn render_login(WithCsrf<LoginContext>) { "pages/login.html" }
|
||||||
|
|
||||||
@ -390,8 +261,9 @@ mod tests {
|
|||||||
#[allow(clippy::disallowed_methods)]
|
#[allow(clippy::disallowed_methods)]
|
||||||
let now = chrono::Utc::now();
|
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 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();
|
templates.check_render(now).await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,28 +49,7 @@ macro_rules! register_templates {
|
|||||||
)*
|
)*
|
||||||
} => {
|
} => {
|
||||||
/// List of registered templates
|
/// List of registered templates
|
||||||
static TEMPLATES: [(&'static str, Option<&'static str>); count!( $( $template )* )] = [
|
static TEMPLATES: [&'static str; count!( $( $template )* )] = [ $( $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)))
|
|
||||||
}
|
|
||||||
) ),* )?
|
|
||||||
];
|
|
||||||
|
|
||||||
impl Templates {
|
impl Templates {
|
||||||
$(
|
$(
|
||||||
|
@ -85,10 +85,10 @@
|
|||||||
"playground": true
|
"playground": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "static"
|
"name": "assets",
|
||||||
|
"path": "./frontend/dist/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"assets": "./frontend/dist/",
|
|
||||||
"manifest": "./frontend/dist/manifest.json",
|
"manifest": "./frontend/dist/manifest.json",
|
||||||
"name": "spa"
|
"name": "spa"
|
||||||
}
|
}
|
||||||
@ -171,8 +171,7 @@
|
|||||||
"templates": {
|
"templates": {
|
||||||
"description": "Configuration related to templates",
|
"description": "Configuration related to templates",
|
||||||
"default": {
|
"default": {
|
||||||
"builtin": true,
|
"path": "./templates/"
|
||||||
"path": null
|
|
||||||
},
|
},
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@ -1402,11 +1401,12 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"static"
|
"assets"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"web_root": {
|
"path": {
|
||||||
"description": "Path from which to serve static files. If not specified, it will serve the static files embedded in the server binary",
|
"description": "Path to the directory to serve.",
|
||||||
|
"default": "./frontend/dist/",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1433,13 +1433,8 @@
|
|||||||
"name"
|
"name"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"assets": {
|
|
||||||
"description": "Path to the assets to server",
|
|
||||||
"default": "./frontend/dist/",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"description": "Path to the vite mamanifest.jsonnifest",
|
"description": "Path to the vite manifest.json",
|
||||||
"default": "./frontend/dist/manifest.json",
|
"default": "./frontend/dist/manifest.json",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -1511,14 +1506,9 @@
|
|||||||
"description": "Configuration related to templates",
|
"description": "Configuration related to templates",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"builtin": {
|
|
||||||
"description": "Load the templates embedded in the binary",
|
|
||||||
"default": true,
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"path": {
|
"path": {
|
||||||
"description": "Path to the folder that holds the custom templates",
|
"description": "Path to the folder which holds the templates",
|
||||||
"default": null,
|
"default": "./templates/",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,8 @@
|
|||||||
"generate": "relay-compiler && eslint --fix .",
|
"generate": "relay-compiler && eslint --fix .",
|
||||||
"lint": "relay-compiler --validate && eslint . && tsc",
|
"lint": "relay-compiler --validate && eslint . && tsc",
|
||||||
"relay": "relay-compiler",
|
"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",
|
"preview": "vite preview",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
|
@ -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");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with 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
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
const base = require("./tailwind.config.cjs");
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
...base,
|
||||||
tailwindcss: {},
|
content: ["../crates/templates/res/**/*.html"],
|
||||||
autoprefixer: {},
|
darkMode: "media",
|
||||||
cssnano: {},
|
};
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user