You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
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: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/crates/static-files/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/frontend/"
|
||||
schedule:
|
||||
|
52
Cargo.lock
generated
52
Cargo.lock
generated
@ -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"
|
||||
|
42
Dockerfile
42
Dockerfile
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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(())
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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> {
|
||||
|
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"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[features]
|
||||
dev = []
|
||||
|
||||
[dependencies]
|
||||
tracing = "0.1.37"
|
||||
tokio = { version = "1.21.2", features = ["macros"] }
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
$(
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
};
|
Reference in New Issue
Block a user