diff --git a/.github/workflows/ci-actions.yaml b/.github/workflows/ci-actions.yaml index d1d37218..1c2f54df 100644 --- a/.github/workflows/ci-actions.yaml +++ b/.github/workflows/ci-actions.yaml @@ -31,3 +31,12 @@ jobs: run: npm run build - name: Test run: npm run test -- --coverage --watchAll=false + - name: Crate Format + working-directory: ./crate + run: cargo fmt --check + - name: Crate Check + working-directory: ./crate + run: cargo check + - name: Crate Clippy + working-directory: ./crate + run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/.gitignore b/.gitignore index 5089c820..cec22ffd 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ npm-debug.log* # Intellij IDEA .idea/ + +crate/target +crate/Cargo.lock diff --git a/crate/Cargo.toml b/crate/Cargo.toml new file mode 100644 index 00000000..62d1c119 --- /dev/null +++ b/crate/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "trustify-ui" +version = "0.1.0" +edition = "2021" + +[dependencies] +static-files = "0.2.1" +serde_json = "1.0.117" +tera = "1.19.1" +base64 = "0.22.1" +serde = { version = "1.0.201", features = ["derive"] } + +[build-dependencies] +static-files = "0.2.1" \ No newline at end of file diff --git a/crate/build.rs b/crate/build.rs new file mode 100644 index 00000000..fe2ac13a --- /dev/null +++ b/crate/build.rs @@ -0,0 +1,69 @@ +use static_files::resource_dir; +use std::path::Path; +use std::process::{Command, ExitStatus}; +use std::{fs, io}; + +static UI_DIR: &str = "../"; +static UI_DIR_SRC: &str = "../src"; +static UI_DIST_DIR: &str = "../client/dist"; +static STATIC_DIR: &str = "target/generated"; + +#[cfg(windows)] +static NPM_CMD: &str = "npm.cmd"; +#[cfg(not(windows))] +static NPM_CMD: &str = "npm"; + +fn main() { + println!("Build Trustify - build.rs!"); + + println!("cargo:rerun-if-changed={}", UI_DIR_SRC); + + let build_ui_status = install_ui_deps() + .and_then(|_| build_ui()) + .and_then(|_| copy_dir_all(UI_DIST_DIR, STATIC_DIR)); + + match build_ui_status { + Ok(_) => println!("UI built successfully"), + Err(_) => panic!("Error while building UI"), + } + + resource_dir("./target/generated").build().unwrap(); +} + +fn install_ui_deps() -> io::Result { + if !Path::new("../node_modules").exists() { + println!("Installing node dependencies..."); + Command::new(NPM_CMD) + .args(["clean-install", "--ignore-scripts"]) + .current_dir(UI_DIR) + .status() + } else { + Ok(ExitStatus::default()) + } +} + +fn build_ui() -> io::Result { + if !Path::new(STATIC_DIR).exists() || Path::new(STATIC_DIR).read_dir()?.next().is_none() { + println!("Building UI..."); + Command::new(NPM_CMD) + .args(["run", "build", "--dev"]) + .current_dir(UI_DIR) + .status() + } else { + Ok(ExitStatus::default()) + } +} + +fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> io::Result<()> { + fs::create_dir_all(&dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let ty = entry.file_type()?; + if ty.is_dir() { + copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?; + } + } + Ok(()) +} diff --git a/crate/src/lib.rs b/crate/src/lib.rs new file mode 100644 index 00000000..2dd3956a --- /dev/null +++ b/crate/src/lib.rs @@ -0,0 +1,108 @@ +include!(concat!(env!("OUT_DIR"), "/generated.rs")); + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use serde::Serialize; +use serde_json::Value; +use static_files::resource::new_resource; +use static_files::Resource; +use std::collections::HashMap; +use std::str::from_utf8; +use std::sync::OnceLock; + +#[derive(Serialize, Clone, Default)] +pub struct UI { + #[serde(rename(serialize = "VERSION"))] + pub version: String, + + #[serde(rename(serialize = "AUTH_REQUIRED"))] + pub auth_required: String, + + #[serde(rename(serialize = "OIDC_SERVER_URL"))] + pub oidc_server_url: String, + + #[serde(rename(serialize = "OIDC_CLIENT_ID"))] + pub oidc_client_id: String, + + #[serde(rename(serialize = "OIDC_SCOPE"))] + pub oidc_scope: String, + + #[serde(rename(serialize = "ANALYTICS_ENABLED"))] + pub analytics_enabled: String, + + #[serde(rename(serialize = "ANALYTICS_WRITE_KEY"))] + pub analytics_write_key: String, +} + +pub fn trustify_ui_resources() -> HashMap<&'static str, Resource> { + let mut resources = generate(); + if let Some(index) = resources.get("index.html.ejs") { + resources.insert( + "index.html", + new_resource(index.data, index.modified, "text/html"), + ); + } + + resources +} + +pub fn generate_index_html( + ui: &UI, + template_file: String, + branding_file_content: String, +) -> tera::Result { + let template = template_file + .replace("<%=", "{{") + .replace("%>", "}}") + .replace( + "?? branding.application.title", + "| default(value=branding.application.title)", + ) + .replace( + "?? branding.application.title", + "| default(value=branding.application.title)", + ); + + let env_json = serde_json::to_string(&ui)?; + let env_base64 = BASE64_STANDARD.encode(env_json.as_bytes()); + + let branding: Value = serde_json::from_str(&branding_file_content)?; + + let mut context = tera::Context::new(); + context.insert("_env", &env_base64); + context.insert("branding", &branding); + + tera::Tera::one_off(&template, &context, true) +} + +pub fn trustify_ui(ui: &UI) -> HashMap<&'static str, Resource> { + let mut resources = generate(); + + let template_file = resources.get("index.html.ejs"); + let branding_file_content = resources.get("branding/strings.json"); + + let index_html = INDEX_HTML.get_or_init(|| { + if let (Some(template_file), Some(branding_file_content)) = + (template_file, branding_file_content) + { + let template_file = + from_utf8(template_file.data).expect("cannot interpret template as UTF-8"); + let branding_file_content = + from_utf8(branding_file_content.data).expect("cannot interpret branding as UTF-8"); + generate_index_html( + ui, + template_file.to_string(), + branding_file_content.to_string(), + ) + .expect("cannot generate index.html") + } else { + "Something went wrong".to_string() + } + }); + + resources.insert("", new_resource(index_html.as_bytes(), 0, "text/html")); + + resources +} + +static INDEX_HTML: OnceLock = OnceLock::new();