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
Frontend/static files building & serving
This commit is contained in:
@ -1,3 +1,6 @@
|
|||||||
target/
|
target/
|
||||||
crates/*/target
|
crates/*/target
|
||||||
|
crates/*/node_modules
|
||||||
.git/
|
.git/
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
45
Cargo.lock
generated
45
Cargo.lock
generated
@ -1539,6 +1539,7 @@ dependencies = [
|
|||||||
"k256",
|
"k256",
|
||||||
"mas-config",
|
"mas-config",
|
||||||
"mas-data-model",
|
"mas-data-model",
|
||||||
|
"mas-static-files",
|
||||||
"mas-templates",
|
"mas-templates",
|
||||||
"mime",
|
"mime",
|
||||||
"oauth2-types",
|
"oauth2-types",
|
||||||
@ -1573,6 +1574,16 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mas-static-files"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"headers",
|
||||||
|
"mime_guess",
|
||||||
|
"rust-embed",
|
||||||
|
"warp",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mas-templates"
|
name = "mas-templates"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -2528,6 +2539,40 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed"
|
||||||
|
version = "6.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d40377bff8cceee81e28ddb73ac97f5c2856ce5522f0b260b763f434cdfae602"
|
||||||
|
dependencies = [
|
||||||
|
"rust-embed-impl",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-impl"
|
||||||
|
version = "6.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94e763e24ba2bf0c72bc6be883f967f794a019fafd1b86ba1daff9c91a7edd30"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"syn",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-utils"
|
||||||
|
version = "7.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ad22c7226e4829104deab21df575e995bfbc4adfad13a595e387477f238c1aec"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
30
Dockerfile
30
Dockerfile
@ -7,12 +7,27 @@
|
|||||||
# there is a small script that translates those platforms to LLVM triples,
|
# there is a small script that translates those platforms to LLVM triples,
|
||||||
# respectively x86-64-unknown-linux-gnu and aarch64-unknown-linux-gnu
|
# respectively x86-64-unknown-linux-gnu and aarch64-unknown-linux-gnu
|
||||||
|
|
||||||
ARG RUSTC_VERSION=1.56.1
|
# The Debian version and version name must be in sync
|
||||||
|
ARG DEBIAN_VERSION=11
|
||||||
|
ARG DEBIAN_VERSION_NAME=bullseye
|
||||||
|
ARG RUSTC_VERSION=1.57.0
|
||||||
|
ARG NODEJS_VERSION=16
|
||||||
|
|
||||||
|
## Build stage that builds the static files/frontend ##
|
||||||
|
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 {} +
|
||||||
|
|
||||||
## Base image with cargo-chef and the right cross-compilation toolchain ##
|
## Base image with cargo-chef and the right cross-compilation toolchain ##
|
||||||
# cargo-chef helps with caching dependencies between builds
|
# cargo-chef helps with caching dependencies between builds
|
||||||
# The image Debian base name (bullseye) must be in sync with the runtime variant (debian11)
|
# The image Debian base name (bullseye) must be in sync with the runtime variant (debian11)
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/rust:${RUSTC_VERSION}-slim-bullseye AS chef
|
FROM --platform=${BUILDPLATFORM} docker.io/library/rust:${RUSTC_VERSION}-slim-${DEBIAN_VERSION_NAME} AS chef
|
||||||
|
|
||||||
# Install x86_64 and aarch64 cross-compiling stack
|
# Install x86_64 and aarch64 cross-compiling stack
|
||||||
RUN apt update && apt install -y --no-install-recommends \
|
RUN apt update && apt install -y --no-install-recommends \
|
||||||
@ -60,6 +75,7 @@ RUN cargo chef cook \
|
|||||||
|
|
||||||
# Build the rest
|
# Build the rest
|
||||||
COPY . .
|
COPY . .
|
||||||
|
COPY --from=static-files /app/crates/static-files/public /app/crates/static-files/public
|
||||||
RUN cargo build \
|
RUN cargo build \
|
||||||
--release \
|
--release \
|
||||||
--bin mas-cli \
|
--bin mas-cli \
|
||||||
@ -68,8 +84,12 @@ RUN cargo build \
|
|||||||
# 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 /mas-cli
|
RUN mv target/$(/docker-arch-to-rust-target.sh "${TARGETPLATFORM}")/release/mas-cli /mas-cli
|
||||||
|
|
||||||
## Runtime stage ##
|
## Runtime stage, debug variant ##
|
||||||
# The image Debian base name (bullseye) must be in sync with the runtime variant (debian11)
|
FROM --platform=${TARGETPLATFORM} gcr.io/distroless/cc-debian${DEBIAN_VERSION}:debug-nonroot AS debug
|
||||||
FROM --platform=${TARGETPLATFORM} gcr.io/distroless/cc-debian11:nonroot
|
COPY --from=builder /mas-cli /mas-cli
|
||||||
|
ENTRYPOINT ["/mas-cli"]
|
||||||
|
|
||||||
|
## Runtime stage ##
|
||||||
|
FROM --platform=${TARGETPLATFORM} gcr.io/distroless/cc-debian${DEBIAN_VERSION}:nonroot
|
||||||
COPY --from=builder /mas-cli /mas-cli
|
COPY --from=builder /mas-cli /mas-cli
|
||||||
ENTRYPOINT ["/mas-cli"]
|
ENTRYPOINT ["/mas-cli"]
|
||||||
|
@ -8,7 +8,15 @@ See the [Documentation](https://matrix-org.github.io/matrix-authentication-servi
|
|||||||
## Running
|
## Running
|
||||||
|
|
||||||
- [Install Rust and Cargo](https://www.rust-lang.org/learn/get-started)
|
- [Install Rust and Cargo](https://www.rust-lang.org/learn/get-started)
|
||||||
|
- [Install Node.js and npm](https://nodejs.org/)
|
||||||
- Clone this repository
|
- Clone this repository
|
||||||
|
- Generate the frontend:
|
||||||
|
```sh
|
||||||
|
cd crates/static-files
|
||||||
|
npm ci
|
||||||
|
npm run build
|
||||||
|
cd ../..
|
||||||
|
```
|
||||||
- Generate the sample config via `cargo run -- config generate > config.yaml`
|
- Generate the sample config via `cargo run -- config generate > config.yaml`
|
||||||
- Run the database migrations via `cargo run -- database migrate`
|
- Run the database migrations via `cargo run -- database migrate`
|
||||||
- Run the server via `cargo run -- server -c config.yaml`
|
- Run the server via `cargo run -- server -c config.yaml`
|
||||||
|
@ -41,7 +41,7 @@ indoc = "1.0.3"
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["otlp", "jaeger", "zipkin"]
|
default = ["otlp", "jaeger", "zipkin"]
|
||||||
dev = ["mas-templates/dev"]
|
dev = ["mas-templates/dev", "mas-core/dev"]
|
||||||
# Enable OpenTelemetry OTLP exporter. Requires "protoc"
|
# Enable OpenTelemetry OTLP exporter. Requires "protoc"
|
||||||
otlp = ["opentelemetry-otlp"]
|
otlp = ["opentelemetry-otlp"]
|
||||||
# Enable OpenTelemetry Jaeger exporter and propagator.
|
# Enable OpenTelemetry Jaeger exporter and propagator.
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
// 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.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -26,12 +28,16 @@ fn default_http_address() -> String {
|
|||||||
pub struct HttpConfig {
|
pub struct HttpConfig {
|
||||||
#[serde(default = "default_http_address")]
|
#[serde(default = "default_http_address")]
|
||||||
pub address: String,
|
pub address: String,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub web_root: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for HttpConfig {
|
impl Default for HttpConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
address: default_http_address(),
|
address: default_http_address(),
|
||||||
|
web_root: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,9 @@ authors = ["Quentin Gliech <quenting@element.io>"]
|
|||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
dev = ["mas-static-files/dev", "mas-templates/dev"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1.14.0", features = ["full"] }
|
tokio = { version = "1.14.0", features = ["full"] }
|
||||||
@ -63,6 +66,7 @@ oauth2-types = { path = "../oauth2-types", features = ["sqlx_type"] }
|
|||||||
mas-config = { path = "../config" }
|
mas-config = { path = "../config" }
|
||||||
mas-data-model = { path = "../data-model" }
|
mas-data-model = { path = "../data-model" }
|
||||||
mas-templates = { path = "../templates" }
|
mas-templates = { path = "../templates" }
|
||||||
|
mas-static-files = { path = "../static-files" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc = "1.0.3"
|
indoc = "1.0.3"
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
#![allow(clippy::unused_async)] // Some warp filters need that
|
#![allow(clippy::unused_async)] // Some warp filters need that
|
||||||
|
|
||||||
use mas_config::RootConfig;
|
use mas_config::RootConfig;
|
||||||
|
use mas_static_files::filter as static_files;
|
||||||
use mas_templates::Templates;
|
use mas_templates::Templates;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use warp::{filters::BoxedFilter, Filter, Reply};
|
use warp::{filters::BoxedFilter, Filter, Reply};
|
||||||
@ -31,7 +32,8 @@ pub fn root(
|
|||||||
templates: &Templates,
|
templates: &Templates,
|
||||||
config: &RootConfig,
|
config: &RootConfig,
|
||||||
) -> BoxedFilter<(impl Reply,)> {
|
) -> BoxedFilter<(impl Reply,)> {
|
||||||
health(pool)
|
static_files(config.http.web_root.clone())
|
||||||
|
.or(health(pool))
|
||||||
.or(oauth2(pool, templates, &config.oauth2, &config.cookies))
|
.or(oauth2(pool, templates, &config.oauth2, &config.cookies))
|
||||||
.or(views(
|
.or(views(
|
||||||
pool,
|
pool,
|
||||||
|
2
crates/static-files/.gitignore
vendored
Normal file
2
crates/static-files/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/node_modules/
|
||||||
|
/public/tailwind.css
|
15
crates/static-files/Cargo.toml
Normal file
15
crates/static-files/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "mas-static-files"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Quentin Gliech <quenting@element.io>"]
|
||||||
|
edition = "2018"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
dev = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
headers = "0.3.5"
|
||||||
|
mime_guess = "2.0.3"
|
||||||
|
rust-embed = "6.3.0"
|
||||||
|
warp = "0.3.2"
|
3996
crates/static-files/package-lock.json
generated
Normal file
3996
crates/static-files/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
crates/static-files/package.json
Normal file
15
crates/static-files/package.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"autoprefixer": "^10.4.0",
|
||||||
|
"cssnano": "^5.0.12",
|
||||||
|
"postcss": "^8.4.4",
|
||||||
|
"tailwindcss": "^2.2.19"
|
||||||
|
}
|
||||||
|
}
|
21
crates/static-files/postcss.config.js
Normal file
21
crates/static-files/postcss.config.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// 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 = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
cssnano: {},
|
||||||
|
}
|
||||||
|
}
|
113
crates/static-files/src/lib.rs
Normal file
113
crates/static-files/src/lib.rs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
#![deny(clippy::all)]
|
||||||
|
#![deny(rustdoc::broken_intra_doc_links)]
|
||||||
|
#![warn(clippy::pedantic)]
|
||||||
|
#![allow(clippy::module_name_repetitions)]
|
||||||
|
#![allow(clippy::missing_panics_doc)]
|
||||||
|
#![allow(clippy::missing_errors_doc)]
|
||||||
|
#![allow(clippy::unused_async)]
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use warp::{filters::BoxedFilter, Filter, Reply};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "dev"))]
|
||||||
|
mod builtin {
|
||||||
|
use std::{convert::TryInto, fmt::Write, str::FromStr};
|
||||||
|
|
||||||
|
use headers::{ContentLength, ContentType, ETag, HeaderMapExt};
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
|
use warp::{
|
||||||
|
filters::BoxedFilter, hyper::StatusCode, path::Tail, reply::Response, Filter, Rejection,
|
||||||
|
Reply,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "public/"]
|
||||||
|
struct Asset;
|
||||||
|
|
||||||
|
async fn serve_embed(
|
||||||
|
path: Tail,
|
||||||
|
if_none_match: Option<String>,
|
||||||
|
) -> Result<Box<dyn Reply>, Rejection> {
|
||||||
|
let path = path.as_str();
|
||||||
|
let asset = Asset::get(path).ok_or_else(warp::reject::not_found)?;
|
||||||
|
|
||||||
|
// TODO: this etag calculation is ugly
|
||||||
|
let etag = {
|
||||||
|
let mut s = String::with_capacity(32 * 2 + 2);
|
||||||
|
write!(s, "\"").unwrap();
|
||||||
|
for b in asset.metadata.sha256_hash() {
|
||||||
|
write!(s, "{:02x}", b).unwrap();
|
||||||
|
}
|
||||||
|
write!(s, "\"").unwrap();
|
||||||
|
s
|
||||||
|
};
|
||||||
|
|
||||||
|
if Some(&etag) == if_none_match.as_ref() {
|
||||||
|
return Ok(Box::new(StatusCode::NOT_MODIFIED));
|
||||||
|
};
|
||||||
|
|
||||||
|
let len = asset.data.len().try_into().unwrap();
|
||||||
|
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||||
|
|
||||||
|
let mut res = Response::new(asset.data.into());
|
||||||
|
res.headers_mut().typed_insert(ContentType::from(mime));
|
||||||
|
res.headers_mut().typed_insert(ContentLength(len));
|
||||||
|
res.headers_mut()
|
||||||
|
.typed_insert(ETag::from_str(&etag).unwrap());
|
||||||
|
Ok(Box::new(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn filter() -> BoxedFilter<(impl Reply,)> {
|
||||||
|
warp::path::tail()
|
||||||
|
.and(warp::filters::header::optional("If-None-Match"))
|
||||||
|
.and_then(serve_embed)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "dev")]
|
||||||
|
mod builtin {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use warp::{filters::BoxedFilter, Reply};
|
||||||
|
|
||||||
|
pub(crate) fn filter() -> BoxedFilter<(impl Reply,)> {
|
||||||
|
let path = PathBuf::from(format!("{}/public", env!("CARGO_MANIFEST_DIR")));
|
||||||
|
super::filter_for_path(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_reply(reply: impl Reply + 'static) -> Box<dyn Reply> {
|
||||||
|
Box::new(reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn filter_for_path(path: PathBuf) -> BoxedFilter<(impl Reply,)> {
|
||||||
|
warp::fs::dir(path).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn filter(path: Option<PathBuf>) -> BoxedFilter<(Box<dyn Reply>,)> {
|
||||||
|
let f = self::builtin::filter();
|
||||||
|
|
||||||
|
if let Some(path) = path {
|
||||||
|
f.or(filter_for_path(path)).map(box_reply).boxed()
|
||||||
|
} else {
|
||||||
|
f.map(box_reply).boxed()
|
||||||
|
}
|
||||||
|
}
|
26
crates/static-files/tailwind.config.js
Normal file
26
crates/static-files/tailwind.config.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// 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",
|
||||||
|
purge: ["../templates/src/res/*.html"],
|
||||||
|
darkMode: false,
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
@ -21,6 +21,7 @@ limitations under the License.
|
|||||||
<title>{% block title %}matrix-authentication-service{% endblock title %}</title>
|
<title>{% block title %}matrix-authentication-service{% endblock title %}</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
|
||||||
|
<link rel="stylesheet" href="/tailwind.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
|
<nav class="navbar is-dark" role="navigation" aria-label="main navigation">
|
||||||
|
Reference in New Issue
Block a user