diff --git a/Cargo.lock b/Cargo.lock index 2f6290db8..0b75db73f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2044,6 +2044,7 @@ dependencies = [ "tracing-serde", "tracing-subscriber", "url", + "urlencoding", "users", "uuid 1.1.2", "validator", @@ -2078,6 +2079,7 @@ dependencies = [ "serde_json", "time 0.2.27", "url", + "urlencoding", "uuid 1.1.2", "webauthn-rs", ] @@ -4169,6 +4171,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" + [[package]] name = "users" version = "0.11.0" diff --git a/Makefile b/Makefile index ec559d395..4444f0c24 100644 --- a/Makefile +++ b/Makefile @@ -142,9 +142,8 @@ docs/pykanidm/build: poetry install && \ poetry run mkdocs build - docs/pykanidm/serve: ## Run the local mkdocs server docs/pykanidm/serve: cd pykanidm && \ poetry install && \ - poetry run mkdocs serve \ No newline at end of file + poetry run mkdocs serve diff --git a/artwork/kani-waving.svg b/artwork/kani-waving.svg new file mode 100644 index 000000000..d6ff475d5 --- /dev/null +++ b/artwork/kani-waving.svg @@ -0,0 +1,168 @@ + + + + + Kanidm Square Logo + + + + + + + + + + + + Kanidm Square Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kanidm_proto/Cargo.toml b/kanidm_proto/Cargo.toml index 83535b93e..63d6727fb 100644 --- a/kanidm_proto/Cargo.toml +++ b/kanidm_proto/Cargo.toml @@ -19,4 +19,4 @@ webauthn-rs = { version = "^0.3.2", default-features = false, features = ["wasm" # Can not upgrade due to breaking timezone apis. time = { version = "=0.2.27", features = ["serde", "std"] } url = { version = "^2.2.2", features = ["serde"] } - +urlencoding = "2.1.0" diff --git a/kanidm_proto/src/v1.rs b/kanidm_proto/src/v1.rs index 0bbd55cf1..a1272511b 100644 --- a/kanidm_proto/src/v1.rs +++ b/kanidm_proto/src/v1.rs @@ -792,22 +792,13 @@ pub struct TotpSecret { impl TotpSecret { /// pub fn to_uri(&self) -> String { - // label = accountname / issuer (“:” / “%3A”) *”%20” accountname - // This is already done server side but paranoia is good! - let accountname = self - .accountname - .replace(':', "") - .replace("%3A", "") - .replace(' ', "%20"); - let issuer = self - .issuer - .replace(':', "") - .replace("%3A", "") - .replace(' ', "%20"); + let accountname = urlencoding::Encoded(&self.accountname); + let issuer = urlencoding::Encoded(&self.issuer); let label = format!("{}:{}", issuer, accountname); let algo = self.algo.to_string(); let secret = self.get_secret(); let period = self.step; + format!( "otpauth://totp/{}?secret={}&issuer={}&algorithm={}&digits=6&period={}", label, secret, issuer, algo, period @@ -991,6 +982,7 @@ mod tests { algo: TotpAlgo::Sha256, }; let s = totp.to_uri(); - assert!(s == "otpauth://totp/blackhats%20australia:william?secret=VK54ZXI&issuer=blackhats%20australia&algorithm=SHA256&digits=6&period=30"); + println!("{}", s); + assert!(s == "otpauth://totp/blackhats%20australia:william%3A%253A?secret=VK54ZXI&issuer=blackhats%20australia&algorithm=SHA256&digits=6&period=30"); } } diff --git a/kanidmd/idm/Cargo.toml b/kanidmd/idm/Cargo.toml index bce183934..3844cd8b4 100644 --- a/kanidmd/idm/Cargo.toml +++ b/kanidmd/idm/Cargo.toml @@ -57,6 +57,7 @@ tracing = { version = "^0.1.35", features = ["attributes"] } tracing-serde = "^0.1.3" tracing-subscriber = { version = "^0.3.14", features = ["env-filter"] } url = { version = "^2.2.2", features = ["serde"] } +urlencoding = "2.1.0" uuid = { version = "^1.1.2", features = ["serde", "v4" ] } validator = { version = "^0.15.0", features = ["phone"] } webauthn-rs = "^0.3.2" diff --git a/kanidmd/idm/src/credential/totp.rs b/kanidmd/idm/src/credential/totp.rs index 52be10226..e70a7a131 100644 --- a/kanidmd/idm/src/credential/totp.rs +++ b/kanidmd/idm/src/credential/totp.rs @@ -165,14 +165,8 @@ impl Totp { pub fn to_proto(&self, accountname: &str, issuer: &str) -> ProtoTotp { ProtoTotp { - accountname: accountname - .replace(":", "") - .replace("%3A", "") - .replace(" ", "%20"), - issuer: issuer - .replace(":", "") - .replace("%3A", "") - .replace(" ", "%20"), + accountname: accountname.to_string(), + issuer: issuer.to_string(), secret: self.secret.clone(), step: self.step, algo: match self.algo { diff --git a/kanidmd/score/src/https/manifest.rs b/kanidmd/score/src/https/manifest.rs index f4d271d66..f12f25a7e 100644 --- a/kanidmd/score/src/https/manifest.rs +++ b/kanidmd/score/src/https/manifest.rs @@ -68,7 +68,7 @@ enum Direction { /// Display modes from the Web app manifest definition /// -/// Ref: https://developer.mozilla.org/en-US/docs/Web/Manifest/display +/// Ref: #[derive(Debug, Clone, Serialize, Deserialize)] enum DisplayMode { /// All of the available display area is used and no user agent chrome is @@ -126,11 +126,15 @@ pub async fn manifest(req: tide::Request) -> tide::Result { }, ]; + let start_url = match req.host() { + Some(value) => format!("https://{}/", value).clone(), + None => String::from("/"), + }; let manifest_struct = Manifest { - short_name: domain_display_name.as_str(), + short_name: "Kanidm", name: domain_display_name.as_str(), - start_url: "/", // TODO: this needs to be the frontend URL, can't get this yet + start_url: start_url.as_str(), display_mode: DisplayMode::MinimalUi, description: None, orientation: None, diff --git a/kanidmd/score/src/https/middleware.rs b/kanidmd/score/src/https/middleware.rs index 643403b97..4afa9fb81 100644 --- a/kanidmd/score/src/https/middleware.rs +++ b/kanidmd/score/src/https/middleware.rs @@ -1,16 +1,159 @@ +///! Custom tide middleware for Kanidm +use crate::https::JavaScriptFile; +use regex::Regex; + +/// This is for the tide_compression middleware so that we only compress certain content types. +/// +/// ``` +/// use score::https::middleware::compression_content_type_checker; +/// let these_should_match = vec![ +/// "application/wasm", +/// "text/json", +/// "text/javascript" +/// ]; +/// for test_value in these_should_match { +/// eprintln!("checking {:?}", test_value); +/// assert!(compression_content_type_checker().is_match(test_value)); +/// } +/// assert!(compression_content_type_checker().is_match("application/wasm")); +/// let these_should_fail = vec![ +/// "image/jpeg", +/// "image/wasm", +/// "text/html", +/// ]; +/// for test_value in these_should_fail { +/// eprintln!("checking {:?}", test_value); +/// assert!(!compression_content_type_checker().is_match(test_value)); +/// } +/// ``` +pub fn compression_content_type_checker() -> Regex { + Regex::new(r"^(?:(image/svg\+xml)|(?:application|text)/(?:css|javascript|json|text|xml|wasm))$") + .expect("regex matcher for tide_compress content-type check failed to compile") +} + +#[derive(Default)] +pub struct CacheableMiddleware; + +#[async_trait::async_trait] +impl tide::Middleware for CacheableMiddleware { + async fn handle( + &self, + request: tide::Request, + next: tide::Next<'_, State>, + ) -> tide::Result { + let mut response = next.run(request).await; + response.insert_header("Cache-Control", "max-age=60,must-revalidate,private"); + Ok(response) + } +} + +#[derive(Default)] +pub struct NoCacheMiddleware; + +#[async_trait::async_trait] +impl tide::Middleware for NoCacheMiddleware { + async fn handle( + &self, + request: tide::Request, + next: tide::Next<'_, State>, + ) -> tide::Result { + let mut response = next.run(request).await; + response.insert_header("Cache-Control", "no-store, max-age=0"); + response.insert_header("Pragma", "no-cache"); + Ok(response) + } +} + +#[derive(Default)] +/// Sets Cache-Control headers on static content endpoints +pub struct StaticContentMiddleware; + +#[async_trait::async_trait] +impl tide::Middleware for StaticContentMiddleware { + async fn handle( + &self, + request: tide::Request, + next: tide::Next<'_, State>, + ) -> tide::Result { + let mut response = next.run(request).await; + response.insert_header("Cache-Control", "max-age=3600,private"); + Ok(response) + } +} + +#[derive(Default)] +/// Adds the folloing headers to responses +/// - x-frame-options +/// - x-content-type-options +/// - cross-origin-resource-policy +/// - cross-origin-embedder-policy +/// - cross-origin-opener-policy +pub struct StrictResponseMiddleware; + +#[async_trait::async_trait] +impl tide::Middleware for StrictResponseMiddleware { + async fn handle( + &self, + request: tide::Request, + next: tide::Next<'_, State>, + ) -> tide::Result { + let mut response = next.run(request).await; + response.insert_header("cross-origin-embedder-policy", "require-corp"); + response.insert_header("cross-origin-opener-policy", "same-origin"); + response.insert_header("cross-origin-resource-policy", "same-origin"); + response.insert_header("x-content-type-options", "nosniff"); + response.insert_header("x-frame-options", "deny"); + Ok(response) + } +} +struct StrictRequestMiddleware; + +impl Default for StrictRequestMiddleware { + fn default() -> Self { + StrictRequestMiddleware {} + } +} + +#[async_trait::async_trait] +impl tide::Middleware for StrictRequestMiddleware { + async fn handle( + &self, + request: tide::Request, + next: tide::Next<'_, State>, + ) -> tide::Result { + let proceed = request + .header("sec-fetch-site") + .map(|hv| { + matches!(hv.as_str(), "same-origin" | "same-site" | "none") + || (request.header("sec-fetch-mode").map(|v| v.as_str()) == Some("navigate") + && request.method() == tide::http::Method::Get + && request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("object") + && request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("embed")) + }) + .unwrap_or(true); + + if proceed { + Ok(next.run(request).await) + } else { + Err(tide::Error::from_str( + tide::StatusCode::MethodNotAllowed, + "StrictRequestViolation", + )) + } + } +} + #[derive(Default)] /// This tide MiddleWare adds headers like Content-Security-Policy /// and similar families. If it keeps adding more things then /// probably rename the middleware :) pub struct UIContentSecurityPolicyResponseMiddleware { // The sha384 hash of /pkg/wasmloader.js - pub integrity_wasmloader: String, + pub hashes: Vec, } impl UIContentSecurityPolicyResponseMiddleware { - pub fn new(integrity_wasmloader: String) -> Self { - return Self { - integrity_wasmloader, - }; + pub fn new(hashes: Vec) -> Self { + return Self { hashes }; } } @@ -26,10 +169,13 @@ impl tide::Middleware ) -> tide::Result { let mut response = next.run(request).await; - // grab the body we're intending to return at this point - let body_str = response.take_body().into_string().await?; - // update it with the hash - response.set_body(body_str.replace("==WASMHASH==", self.integrity_wasmloader.as_str())); + // a list of hashes of js files that we're sending to the user + let hashes: Vec = self + .hashes + .iter() + .map(|j| format!("'{}'", j.hash)) + .collect(); + response.insert_header( /* content-security-policy headers tell the browser what to trust https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy @@ -39,16 +185,12 @@ impl tide::Middleware we should be loading, and should be really secure about that! */ - // TODO: consider scraping the other js files that wasm-pack builds and including them too "content-security-policy", vec![ "default-src 'self'", + // TODO: #912 have a dev/test mode where we can rebuild the hashes on page load, so when doing constant JS changes/rebuilds we don't have to restart the server every time. It'd be *terrible* to run in prod because of the constant disk thrashing, but nicer for devs. // we need unsafe-eval because of WASM things - format!( - "script-src 'self' 'sha384-{}' 'unsafe-eval'", - self.integrity_wasmloader.as_str() - ) - .as_str(), + format!("script-src 'self' {} 'unsafe-eval'", hashes.join(" ")).as_str(), "form-action https: 'self'", // to allow for OAuth posts // we are not currently using workers so it can be blocked "worker-src 'none'", diff --git a/kanidmd/score/src/https/mod.rs b/kanidmd/score/src/https/mod.rs index 1d32915c5..bd2ecdf99 100644 --- a/kanidmd/score/src/https/mod.rs +++ b/kanidmd/score/src/https/mod.rs @@ -1,5 +1,5 @@ mod manifest; -mod middleware; +pub mod middleware; mod oauth2; mod v1; @@ -15,7 +15,6 @@ use kanidm::config::{ServerRole, TlsConfiguration}; use kanidm::prelude::*; use kanidm::status::StatusActor; use kanidm::tracing_tree::TreeMiddleware; -use regex::Regex; use serde::Serialize; use std::fs::canonicalize; use std::path::PathBuf; @@ -25,6 +24,35 @@ use tide_openssl::TlsListener; use tracing::{error, info}; use uuid::Uuid; +#[derive(Clone)] +pub struct JavaScriptFile { + // Relative to the pkg/ dir + filepath: &'static str, + // SHA384 hash of the file + hash: String, + // if it's a module add the "type" + filetype: Option, +} + +impl JavaScriptFile { + /// return the hash for use in CSP headers + pub fn as_csp_hash(self) -> String { + self.hash + } + + /// returns a "#, + self.filepath, self.hash, typeattr, + ) + } +} + #[derive(Clone)] pub struct AppState { pub status_ref: &'static StatusActor, @@ -33,36 +61,10 @@ pub struct AppState { // Store the token management parts. pub jws_signer: std::sync::Arc, pub jws_validator: std::sync::Arc, + /// The SHA384 hashes of javascript files we're going to serve to users + pub js_files: Vec, } -/// This is for the tide_compression middleware so that we only compress certain content types. -/// -/// ``` -/// use score::https::compression_content_type_checker; -/// let these_should_match = vec![ -/// "application/wasm", -/// "text/json", -/// "text/javascript" -/// ]; -/// for test_value in these_should_match { -/// eprintln!("checking {:?}", test_value); -/// assert!(compression_content_type_checker().is_match(test_value)); -/// } -/// assert!(compression_content_type_checker().is_match("application/wasm")); -/// let these_should_fail = vec![ -/// "image/jpeg", -/// "image/wasm", -/// "text/html", -/// ]; -/// for test_value in these_should_fail { -/// eprintln!("checking {:?}", test_value); -/// assert!(!compression_content_type_checker().is_match(test_value)); -/// } -/// ``` -pub fn compression_content_type_checker() -> Regex { - Regex::new(r"^(?:(image/svg\+xml)|(?:application|text)/(?:css|javascript|json|text|xml|wasm))$") - .expect("regex matcher for tide_compress content-type check failed to compile") -} pub trait RequestExtensions { fn get_current_uat(&self) -> Option; @@ -182,7 +184,7 @@ pub fn to_tide_response( }) } -/// Returns a generic robots.txt +/// Returns a generic robots.txt blocking all bots async fn robots_txt(_req: tide::Request) -> tide::Result { let mut res = tide::Response::new(200); @@ -205,6 +207,15 @@ async fn index_view(req: tide::Request) -> tide::Result { res.insert_header("X-KANIDM-OPID", hvalue); res.set_content_type("text/html;charset=utf-8"); + // this feels icky but I felt that adding a trait on Vec which generated the string was going a bit far + let jsfiles: Vec = req + .state() + .to_owned() + .js_files + .into_iter() + .map(|j| j.as_tag()) + .collect(); + let jstags = jsfiles.join(" "); res.set_body(format!(r#" @@ -223,8 +234,8 @@ async fn index_view(req: tide::Request) -> tide::Result { - - + {} +
@@ -239,125 +250,16 @@ async fn index_view(req: tide::Request) -> tide::Result { -"#, domain_display_name.as_str() +"#, + domain_display_name.as_str(), + jstags, ) ); Ok(res) } -#[derive(Default)] -struct NoCacheMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for NoCacheMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - response.insert_header("Cache-Control", "no-store, max-age=0"); - response.insert_header("Pragma", "no-cache"); - Ok(response) - } -} - -#[derive(Default)] -struct CacheableMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for CacheableMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - response.insert_header("Cache-Control", "max-age=60,must-revalidate,private"); - Ok(response) - } -} - -#[derive(Default)] -/// Sets Cache-Control headers on static content endpoints -struct StaticContentMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for StaticContentMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - response.insert_header("Cache-Control", "max-age=3600,private"); - Ok(response) - } -} - -#[derive(Default)] -/// Adds the folloing headers to responses -/// - x-frame-options -/// - x-content-type-options -/// - cross-origin-resource-policy -/// - cross-origin-embedder-policy -/// - cross-origin-opener-policy -struct StrictResponseMiddleware; - -#[async_trait::async_trait] -impl tide::Middleware for StrictResponseMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let mut response = next.run(request).await; - response.insert_header("cross-origin-embedder-policy", "require-corp"); - response.insert_header("cross-origin-opener-policy", "same-origin"); - response.insert_header("cross-origin-resource-policy", "same-origin"); - response.insert_header("x-content-type-options", "nosniff"); - response.insert_header("x-frame-options", "deny"); - Ok(response) - } -} -struct StrictRequestMiddleware; - -impl Default for StrictRequestMiddleware { - fn default() -> Self { - StrictRequestMiddleware {} - } -} - -#[async_trait::async_trait] -impl tide::Middleware for StrictRequestMiddleware { - async fn handle( - &self, - request: tide::Request, - next: tide::Next<'_, State>, - ) -> tide::Result { - let proceed = request - .header("sec-fetch-site") - .map(|hv| { - matches!(hv.as_str(), "same-origin" | "same-site" | "none") - || (request.header("sec-fetch-mode").map(|v| v.as_str()) == Some("navigate") - && request.method() == tide::http::Method::Get - && request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("object") - && request.header("sec-fetch-dest").map(|v| v.as_str()) != Some("embed")) - }) - .unwrap_or(true); - - if proceed { - Ok(next.run(request).await) - } else { - Err(tide::Error::from_str( - tide::StatusCode::MethodNotAllowed, - "StrictRequestViolation", - )) - } - } -} - +/// Generates the integrity hash for a file based on a filename pub fn generate_integrity_hash(filename: String) -> Result { let wasm_filepath = PathBuf::from(filename); match wasm_filepath.exists() { @@ -379,7 +281,7 @@ pub fn generate_integrity_hash(filename: String) -> Result { }; let shasum = openssl::hash::hash(openssl::hash::MessageDigest::sha384(), &filecontents).unwrap(); - Ok(format!("{}", openssl::base64::encode_block(&shasum))) + Ok(format!("sha384-{}", openssl::base64::encode_block(&shasum))) } } } @@ -403,12 +305,44 @@ pub fn create_https_server( let jws_validator = std::sync::Arc::new(jws_validator); let jws_signer = std::sync::Arc::new(jws_signer); + let mut js_files: Vec = Vec::new(); + + if !matches!(role, ServerRole::WriteReplicaNoUI) { + // let's set up the list of js module hashes + for filepath in ["wasmloader.js"] { + js_files.push(JavaScriptFile { + filepath, + hash: generate_integrity_hash(format!( + "{}/{}", + env!("KANIDM_WEB_UI_PKG_PATH").to_owned(), + filepath, + )) + .unwrap(), + filetype: Some("module".to_string()), + }); + } + // let's set up the list of non-module hashes + for filepath in ["external/bootstrap.bundle.min.js"] { + js_files.push(JavaScriptFile { + filepath, + hash: generate_integrity_hash(format!( + "{}/{}", + env!("KANIDM_WEB_UI_PKG_PATH").to_owned(), + filepath, + )) + .unwrap(), + filetype: None, + }); + } + }; + let mut tserver = tide::Server::with_state(AppState { status_ref, qe_w_ref, qe_r_ref, jws_signer, jws_validator, + js_files: js_files.to_owned(), }); // tide::log::with_level(tide::log::LevelFilter::Debug); @@ -467,10 +401,7 @@ pub fn create_https_server( let mut static_tserver = tserver.at(""); static_tserver.with(StaticContentMiddleware::default()); - static_tserver.with(UIContentSecurityPolicyResponseMiddleware::new( - generate_integrity_hash(env!("KANIDM_WEB_UI_PKG_PATH").to_owned() + "/wasmloader.js") - .unwrap(), - )); + static_tserver.with(UIContentSecurityPolicyResponseMiddleware::new(js_files)); // The compression middleware needs to be the last one added before routes static_tserver.with(compress_middleware.clone()); diff --git a/kanidmd_web_ui/pkg/img/kani-waving.svg b/kanidmd_web_ui/pkg/img/kani-waving.svg new file mode 100644 index 000000000..d6ff475d5 --- /dev/null +++ b/kanidmd_web_ui/pkg/img/kani-waving.svg @@ -0,0 +1,168 @@ + + + + + Kanidm Square Logo + + + + + + + + + + + + Kanidm Square Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kanidmd_web_ui/pkg/kanidmd_web_ui.js b/kanidmd_web_ui/pkg/kanidmd_web_ui.js index 076446b93..01217bf35 100644 --- a/kanidmd_web_ui/pkg/kanidmd_web_ui.js +++ b/kanidmd_web_ui/pkg/kanidmd_web_ui.js @@ -1,14 +1,10 @@ -import { modal_hide } from './snippets/kanidmd_web_ui-273c66c330cf4c44/inline0.js'; +import { modal_hide_by_id } from '/pkg/wasmloader.js'; let wasm; -const heap = new Array(32).fill(undefined); +const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); -heap.push(undefined, null, true, false); - -function getObject(idx) { return heap[idx]; } - -let WASM_VECTOR_LEN = 0; +cachedTextDecoder.decode(); let cachedUint8Memory0; function getUint8Memory0() { @@ -18,6 +14,31 @@ function getUint8Memory0() { return cachedUint8Memory0; } +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + +const heap = new Array(32).fill(undefined); + +heap.push(undefined, null, true, false); + +let heap_next = heap.length; + +function addHeapObject(obj) { + if (heap_next === heap.length) heap.push(heap.length + 1); + const idx = heap_next; + heap_next = heap[idx]; + + if (typeof(heap_next) !== 'number') throw new Error('corrupt heap'); + + heap[idx] = obj; + return idx; +} + +function getObject(idx) { return heap[idx]; } + +let WASM_VECTOR_LEN = 0; + const cachedTextEncoder = new TextEncoder('utf-8'); const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' @@ -73,6 +94,10 @@ function passStringToWasm0(arg, malloc, realloc) { return ptr; } +function isLikeNone(x) { + return x === undefined || x === null; +} + let cachedInt32Memory0; function getInt32Memory0() { if (cachedInt32Memory0.byteLength === 0) { @@ -81,31 +106,6 @@ function getInt32Memory0() { return cachedInt32Memory0; } -const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - -cachedTextDecoder.decode(); - -function getStringFromWasm0(ptr, len) { - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - -let heap_next = heap.length; - -function addHeapObject(obj) { - if (heap_next === heap.length) heap.push(heap.length + 1); - const idx = heap_next; - heap_next = heap[idx]; - - if (typeof(heap_next) !== 'number') throw new Error('corrupt heap'); - - heap[idx] = obj; - return idx; -} - -function isLikeNone(x) { - return x === undefined || x === null; -} - function _assertBoolean(n) { if (typeof(n) !== 'boolean') { throw new Error('expected a boolean argument'); @@ -367,17 +367,6 @@ async function load(module, imports) { function getImports() { const imports = {}; imports.wbg = {}; - imports.wbg.__wbg_modalhide_673016763df325bd = function() { return logError(function (arg0, arg1) { - modal_hide(getStringFromWasm0(arg0, arg1)); - }, arguments) }; - imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { - const obj = getObject(arg1); - const ret = JSON.stringify(obj === undefined ? null : obj); - const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - getInt32Memory0()[arg0 / 4 + 1] = len0; - getInt32Memory0()[arg0 / 4 + 0] = ptr0; - }; imports.wbg.__wbindgen_string_new = function(arg0, arg1) { const ret = getStringFromWasm0(arg0, arg1); return addHeapObject(ret); @@ -394,6 +383,17 @@ function getImports() { const ret = getObject(arg0); return addHeapObject(ret); }; + imports.wbg.__wbg_modalhidebyid_b9efcd5f48cb1c79 = function() { return logError(function (arg0, arg1) { + modal_hide_by_id(getStringFromWasm0(arg0, arg1)); + }, arguments) }; + imports.wbg.__wbindgen_json_serialize = function(arg0, arg1) { + const obj = getObject(arg1); + const ret = JSON.stringify(obj === undefined ? null : obj); + const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + getInt32Memory0()[arg0 / 4 + 1] = len0; + getInt32Memory0()[arg0 / 4 + 0] = ptr0; + }; imports.wbg.__wbindgen_json_parse = function(arg0, arg1) { const ret = JSON.parse(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); @@ -453,6 +453,11 @@ function getImports() { _assertBoolean(ret); return ret; }; + imports.wbg.__wbg_debug_5a27eb2cb0d074ba = function() { return logError(function (arg0, arg1) { + var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); + wasm.__wbindgen_free(arg0, arg1 * 4); + console.debug(...v0); + }, arguments) }; imports.wbg.__wbg_log_06b7ffc63a0f8bee = function() { return logError(function (arg0, arg1) { var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice(); wasm.__wbindgen_free(arg0, arg1 * 4); @@ -866,16 +871,16 @@ function getImports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper19331 = function() { return logError(function (arg0, arg1, arg2) { - const ret = makeClosure(arg0, arg1, 1306, __wbg_adapter_30); + imports.wbg.__wbindgen_closure_wrapper19372 = function() { return logError(function (arg0, arg1, arg2) { + const ret = makeClosure(arg0, arg1, 1308, __wbg_adapter_30); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbindgen_closure_wrapper23261 = function() { return logError(function (arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 1331, __wbg_adapter_33); + imports.wbg.__wbindgen_closure_wrapper23302 = function() { return logError(function (arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1333, __wbg_adapter_33); return addHeapObject(ret); }, arguments) }; - imports.wbg.__wbindgen_closure_wrapper23862 = function() { return logError(function (arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 1357, __wbg_adapter_36); + imports.wbg.__wbindgen_closure_wrapper23903 = function() { return logError(function (arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 1359, __wbg_adapter_36); return addHeapObject(ret); }, arguments) }; diff --git a/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm b/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm index 89b533d5c..deaf871f1 100644 Binary files a/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm and b/kanidmd_web_ui/pkg/kanidmd_web_ui_bg.wasm differ diff --git a/kanidmd_web_ui/pkg/snippets/kanidmd_web_ui-273c66c330cf4c44/inline0.js b/kanidmd_web_ui/pkg/snippets/kanidmd_web_ui-273c66c330cf4c44/inline0.js deleted file mode 100644 index 939b16354..000000000 --- a/kanidmd_web_ui/pkg/snippets/kanidmd_web_ui-273c66c330cf4c44/inline0.js +++ /dev/null @@ -1,5 +0,0 @@ -export function modal_hide(m) { - var elem = document.getElementById(m); - var modal = bootstrap.Modal.getInstance(elem); - modal.hide(); -} \ No newline at end of file diff --git a/kanidmd_web_ui/pkg/style.css b/kanidmd_web_ui/pkg/style.css index 8f7a8fcc6..e52eb5261 100644 --- a/kanidmd_web_ui/pkg/style.css +++ b/kanidmd_web_ui/pkg/style.css @@ -19,7 +19,6 @@ body { } .form-signin { - /* width: 100%; */ max-width: 680px; margin: auto; } diff --git a/kanidmd_web_ui/pkg/wasmloader.js b/kanidmd_web_ui/pkg/wasmloader.js index ed5cabb87..45b57524e 100644 --- a/kanidmd_web_ui/pkg/wasmloader.js +++ b/kanidmd_web_ui/pkg/wasmloader.js @@ -4,4 +4,12 @@ async function main() { await init('/pkg/kanidmd_web_ui_bg.wasm'); run_app(); } -main() \ No newline at end of file +main() + +// this is used in +export function modal_hide_by_id(m) { + var elem = document.getElementById(m); + var modal = bootstrap.Modal.getInstance(elem); + modal.hide(); +}; + diff --git a/kanidmd_web_ui/src/constants.rs b/kanidmd_web_ui/src/constants.rs new file mode 100644 index 000000000..f6e9639b5 --- /dev/null +++ b/kanidmd_web_ui/src/constants.rs @@ -0,0 +1,7 @@ +///! Constants + +// CSS classes that get applied to full-page forms +pub const CSS_CLASSES_BODY_FORM: &[&str] = &["flex-column", "d-flex", "h-100"]; + +// the HTML element ID that the signout modal dialogue box has +pub const ID_SIGNOUTMODAL: &str = "signoutModal"; diff --git a/kanidmd_web_ui/src/credential/delete.rs b/kanidmd_web_ui/src/credential/delete.rs index 0f2b97650..18d8d4b97 100644 --- a/kanidmd_web_ui/src/credential/delete.rs +++ b/kanidmd_web_ui/src/credential/delete.rs @@ -74,9 +74,7 @@ impl DeleteApp { let jsval = JsFuture::from(resp.json()?).await?; let status: CUStatus = jsval.into_serde().expect_throw("Invalid response type"); - EventBus::dispatcher().send(EventBusMsg::UpdateStatus { - status: status.clone(), - }); + EventBus::dispatcher().send(EventBusMsg::UpdateStatus { status }); Ok(Msg::Success) } else { @@ -142,10 +140,7 @@ impl Component for DeleteApp { fn view(&self, ctx: &Context) -> Html { console::log!("delete modal::view"); - let submit_enabled = match &self.state { - State::Init => true, - _ => false, - }; + let submit_enabled = matches!(&self.state, State::Init); html! {