mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-22 20:26:30 +01:00
Splitting the SPAs (#2219)
* doing some work for enumerating how the accounts work together * fixing up build scripts and removing extra things * making JavaScript as_tag use the struct field names * making shared.js a module, removing wasmloader.js * don't compress compressed things
This commit is contained in:
parent
ad3c491d07
commit
e02328ae8b
11
.codespell_ignore
Normal file
11
.codespell_ignore
Normal file
|
@ -0,0 +1,11 @@
|
|||
alledges
|
||||
crate
|
||||
unexpect
|
||||
Pres
|
||||
pres
|
||||
ACI
|
||||
aci
|
||||
ser
|
||||
te
|
||||
ue
|
||||
unx
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -30,3 +30,7 @@ kanidm-client-tools.tar.gz
|
|||
.coverage
|
||||
pykanidm/dist/
|
||||
pykanidm/site/
|
||||
|
||||
# oauth2 integration test things
|
||||
scripts/oauth_proxy/client.secret
|
||||
scripts/oauth_proxy/envfile
|
||||
|
|
101
Cargo.lock
generated
101
Cargo.lock
generated
|
@ -2259,7 +2259,7 @@ dependencies = [
|
|||
"gloo-render",
|
||||
"gloo-storage",
|
||||
"gloo-timers 0.2.6",
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.1.7",
|
||||
"gloo-worker",
|
||||
]
|
||||
|
||||
|
@ -2269,7 +2269,7 @@ version = "0.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
|
||||
dependencies = [
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.1.7",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
|
@ -2316,7 +2316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f"
|
||||
dependencies = [
|
||||
"gloo-events",
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.1.7",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.5.0",
|
||||
"serde_urlencoded",
|
||||
|
@ -2334,7 +2334,7 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.1.7",
|
||||
"http",
|
||||
"js-sys",
|
||||
"pin-project",
|
||||
|
@ -2362,7 +2362,7 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
|
||||
dependencies = [
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.1.7",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -2406,6 +2406,19 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-utils"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-worker"
|
||||
version = "0.2.1"
|
||||
|
@ -2415,7 +2428,7 @@ dependencies = [
|
|||
"anymap2",
|
||||
"bincode",
|
||||
"gloo-console",
|
||||
"gloo-utils",
|
||||
"gloo-utils 0.1.7",
|
||||
"js-sys",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
|
@ -3059,6 +3072,7 @@ dependencies = [
|
|||
"filetime",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"hashbrown 0.14.2",
|
||||
"http",
|
||||
"hyper",
|
||||
"kanidm_build_profiles",
|
||||
|
@ -3196,13 +3210,85 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "kanidmd_web_ui"
|
||||
name = "kanidmd_web_ui_admin"
|
||||
version = "1.1.0-rc.14-dev"
|
||||
dependencies = [
|
||||
"gloo",
|
||||
"gloo-utils 0.2.0",
|
||||
"js-sys",
|
||||
"kanidm_proto",
|
||||
"kanidmd_web_ui_shared",
|
||||
"lazy_static",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.5.0",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
"yew",
|
||||
"yew-router",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kanidmd_web_ui_login_flows"
|
||||
version = "1.1.0-rc.14-dev"
|
||||
dependencies = [
|
||||
"gloo",
|
||||
"gloo-utils 0.2.0",
|
||||
"js-sys",
|
||||
"kanidm_proto",
|
||||
"kanidmd_web_ui_shared",
|
||||
"lazy_static",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.5.0",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
"yew",
|
||||
"yew-router",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kanidmd_web_ui_shared"
|
||||
version = "1.1.0-rc.14-dev"
|
||||
dependencies = [
|
||||
"gloo",
|
||||
"js-sys",
|
||||
"kanidm_proto",
|
||||
"lazy_static",
|
||||
"serde",
|
||||
"serde-wasm-bindgen 0.5.0",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-bindgen-test",
|
||||
"web-sys",
|
||||
"yew",
|
||||
"yew-router",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kanidmd_web_ui_user"
|
||||
version = "1.1.0-rc.14-dev"
|
||||
dependencies = [
|
||||
"gloo",
|
||||
"gloo-timers 0.3.0",
|
||||
"gloo-utils 0.2.0",
|
||||
"js-sys",
|
||||
"kanidm_proto",
|
||||
"kanidmd_web_ui_shared",
|
||||
"lazy_static",
|
||||
"qrcode",
|
||||
"regex",
|
||||
|
@ -4088,6 +4174,7 @@ dependencies = [
|
|||
"fixedbitset",
|
||||
"indexmap 2.0.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -13,7 +13,10 @@ members = [
|
|||
"unix_integration",
|
||||
"unix_integration/nss_kanidm",
|
||||
"unix_integration/pam_kanidm",
|
||||
"server/web_ui",
|
||||
"server/web_ui/admin",
|
||||
"server/web_ui/login_flows",
|
||||
"server/web_ui/user",
|
||||
"server/web_ui/shared",
|
||||
"server/daemon",
|
||||
"server/lib",
|
||||
"server/lib-macros",
|
||||
|
@ -125,6 +128,7 @@ futures-concurrency = "^3.1.0"
|
|||
futures-util = { version = "^0.3.21", features = ["sink"] }
|
||||
gix = { version = "0.53.1", default-features = false }
|
||||
gloo = "^0.8.1"
|
||||
gloo-utils = "0.2.0"
|
||||
hashbrown = { version = "0.14.2", features = ["serde", "inline-more", "ahash"] }
|
||||
hex = "^0.4.3"
|
||||
http = "0.2.9"
|
||||
|
@ -138,6 +142,7 @@ image = { version = "0.24.7", default-features = false, features = [
|
|||
] }
|
||||
enum-iterator = "1.4.0"
|
||||
js-sys = "^0.3.63"
|
||||
kanidmd_web_ui_shared = { path = "./server/web_ui/shared" }
|
||||
# REMOVE this
|
||||
lazy_static = "^1.4.0"
|
||||
ldap3_client = "^0.3.5"
|
||||
|
|
9
Makefile
9
Makefile
|
@ -133,7 +133,7 @@ install-tools:
|
|||
codespell: ## spell-check things.
|
||||
codespell:
|
||||
codespell -c \
|
||||
-L 'crate,unexpect,Pres,pres,ACI,aci,ser,te,ue,unx,aNULL' \
|
||||
--ignore-words .codespell_ignore \
|
||||
--skip='./target,./pykanidm/.venv,./pykanidm/.mypy_cache,./.mypy_cache,./pykanidm/poetry.lock' \
|
||||
--skip='./book/*.js' \
|
||||
--skip='./book/book/*' \
|
||||
|
@ -141,7 +141,10 @@ codespell:
|
|||
--skip='./docs/*,./.git' \
|
||||
--skip='*.svg' \
|
||||
--skip='./rlm_python/mods-available/eap' \
|
||||
--skip='./server/web_ui/static/external,./server/web_ui/pkg/external' \
|
||||
--skip='./server/web_ui/static/external' \
|
||||
--skip='./server/web_ui/pkg/external' \
|
||||
--skip='./server/web_ui/shared/static/external' \
|
||||
--skip='./server/web_ui/admin/static/external,./server/web_ui/admin/pkg/external' \
|
||||
--skip='./server/lib/src/constants/system_config.rs,./pykanidm/site,./server/lib/src/constants/*.json'
|
||||
|
||||
.PHONY: test/pykanidm/pytest
|
||||
|
@ -277,7 +280,7 @@ cert/clean:
|
|||
|
||||
.PHONY: webui
|
||||
webui: ## Build the WASM web frontend
|
||||
cd server/web_ui && ./build_wasm_release.sh
|
||||
cd server/web_ui && ./build_wasm.sh
|
||||
|
||||
.PHONY: webui/test
|
||||
webui/test: ## Run wasm-pack test
|
||||
|
|
|
@ -276,7 +276,7 @@ cd server/web_ui/
|
|||
./build_wasm_dev.sh
|
||||
```
|
||||
|
||||
To build for release, run `build_wasm_release.sh`.
|
||||
To build for release, run `build_wasm.sh`, or `make webui` from the project root.
|
||||
|
||||
The "developer" profile for kanidmd will automatically use the pkg output in this folder.
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ use std::os::unix::fs::MetadataExt;
|
|||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use kanidm_proto::constants::uri::V1_AUTH_VALID;
|
||||
use kanidm_proto::constants::{APPLICATION_JSON, ATTR_NAME, KOPID, KSESSIONID, KVERSION};
|
||||
use kanidm_proto::v1::*;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
|
@ -1442,7 +1443,7 @@ impl KanidmClient {
|
|||
}
|
||||
|
||||
pub async fn auth_valid(&self) -> Result<(), ClientError> {
|
||||
self.perform_get_request("/v1/auth/valid").await
|
||||
self.perform_get_request(V1_AUTH_VALID).await
|
||||
}
|
||||
|
||||
pub async fn whoami(&self) -> Result<Option<Entry>, ClientError> {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "kanidm_lib_crypto"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = { workspace = true }
|
||||
|
||||
[features]
|
||||
tpm = ["dep:tss-esapi"]
|
||||
|
|
|
@ -17,7 +17,7 @@ doctest = false
|
|||
[dependencies]
|
||||
|
||||
[target.'cfg(target_family = "windows")'.dependencies]
|
||||
whoami = {workspace = true}
|
||||
whoami = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_family = "windows"))'.dependencies]
|
||||
kanidm_utils_users = { workspace = true }
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
//! Because consistency is great!
|
||||
//!
|
||||
pub mod uri;
|
||||
|
||||
pub const CONTENT_TYPE_JPG: &str = "image/jpeg";
|
||||
pub const CONTENT_TYPE_PNG: &str = "image/png";
|
||||
|
@ -6,7 +8,7 @@ pub const CONTENT_TYPE_GIF: &str = "image/gif";
|
|||
pub const CONTENT_TYPE_SVG: &str = "image/svg+xml";
|
||||
pub const CONTENT_TYPE_WEBP: &str = "image/webp";
|
||||
|
||||
// for when the user uploads things to the various image endpoints
|
||||
// For when the user uploads things to the various image endpoints, these are the valid content-types.
|
||||
pub const VALID_IMAGE_UPLOAD_CONTENT_TYPES: [&str; 5] = [
|
||||
CONTENT_TYPE_JPG,
|
||||
CONTENT_TYPE_PNG,
|
||||
|
@ -22,13 +24,14 @@ pub const DEFAULT_CLIENT_CONFIG_PATH: &str = "/etc/kanidm/config";
|
|||
/// The user-owned path for Kanidm client config
|
||||
pub const DEFAULT_CLIENT_CONFIG_PATH_HOME: &str = "~/.config/kanidm";
|
||||
|
||||
/// The default bind address for the Kanidm server
|
||||
/// The default HTTPS bind address for the Kanidm server
|
||||
pub const DEFAULT_SERVER_ADDRESS: &str = "127.0.0.1:8443";
|
||||
pub const DEFAULT_SERVER_LOCALHOST: &str = "localhost:8443";
|
||||
/// The default LDAP bind address for the Kanidm server
|
||||
pub const DEFAULT_LDAP_ADDRESS: &str = "127.0.0.1:636";
|
||||
pub const DEFAULT_LDAP_LOCALHOST: &str = "localhost:636";
|
||||
|
||||
/// IF YOU CHANGE THESE VALUES YOU BREAK EVERYTHING
|
||||
// IF YOU CHANGE THESE VALUES YOU BREAK EVERYTHING
|
||||
pub const ATTR_ACCOUNT_EXPIRE: &str = "account_expire";
|
||||
pub const ATTR_ACCOUNT_VALID_FROM: &str = "account_valid_from";
|
||||
pub const ATTR_ACCOUNT: &str = "account";
|
||||
|
@ -185,15 +188,18 @@ pub const LDAP_ATTR_OBJECTCLASS: &str = "objectClass";
|
|||
pub const LDAP_ATTR_OU: &str = "ou";
|
||||
pub const LDAP_CLASS_GROUPOFNAMES: &str = "groupofnames";
|
||||
|
||||
// rust can't deal with this being compiled out, don't try and #[cfg()] them
|
||||
// Rust can't deal with this being compiled out, don't try and #[cfg()] them
|
||||
pub const TEST_ATTR_NON_EXIST: &str = "non-exist";
|
||||
pub const TEST_ATTR_TEST_ATTR: &str = "testattr";
|
||||
pub const TEST_ATTR_EXTRA: &str = "extra";
|
||||
pub const TEST_ATTR_NUMBER: &str = "testattrnumber";
|
||||
pub const TEST_ATTR_NOTALLOWED: &str = "notallowed";
|
||||
|
||||
/// HTTP Header containing an auth session ID for when you're going through an auth flow
|
||||
pub const KSESSIONID: &str = "X-KANIDM-AUTH-SESSION-ID";
|
||||
/// HTTP Header containing the backend operation ID
|
||||
pub const KOPID: &str = "X-KANIDM-OPID";
|
||||
/// HTTP Header containing the Kanidm server version
|
||||
pub const KVERSION: &str = "X-KANIDM-VERSION";
|
||||
|
||||
pub const X_FORWARDED_FOR: &str = "x-forwarded-for";
|
||||
|
|
16
proto/src/constants/uri.rs
Normal file
16
proto/src/constants/uri.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
//! Shared URIs
|
||||
//!
|
||||
//! ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
//!
|
||||
//! IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS EVERYWHERE
|
||||
//!
|
||||
//! SERIOUSLY... DO NOT CHANGE THEM!
|
||||
//!
|
||||
/// ⚠️ ⚠️ WARNING DO NOT CHANGE THIS ⚠️ ⚠️
|
||||
pub const OAUTH2_AUTHORISE: &str = "/oauth2/authorise";
|
||||
/// ⚠️ ⚠️ WARNING DO NOT CHANGE THIS ⚠️ ⚠️
|
||||
pub const OAUTH2_AUTHORISE_PERMIT: &str = "/oauth2/authorise/permit";
|
||||
/// ⚠️ ⚠️ WARNING DO NOT CHANGE THIS ⚠️ ⚠️
|
||||
pub const OAUTH2_AUTHORISE_REJECT: &str = "/oauth2/authorise/reject";
|
||||
|
||||
pub const V1_AUTH_VALID: &str = "/v1/auth/valid";
|
9
scripts/oauth_proxy/README.md
Normal file
9
scripts/oauth_proxy/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# OAuth Proxy Test
|
||||
|
||||
This dir has some things for setting up a simple OAuth2 RS so things can get tested.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
1. Run the `setup_dev_environment.sh` script and set a credential for `testuser`.
|
||||
2. Look for the OAuth2 Secret in the script output and copy it into a file called `client.secret` in this dir.
|
||||
3. Run `./run_proxy.sh` to start the proxy, and then go to the URL and do the thing!
|
12
scripts/oauth_proxy/index.html
Normal file
12
scripts/oauth_proxy/index.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>OAuth Test Proxy</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Success!</h1>
|
||||
|
||||
<p>You have successfully authenticated with the OAuth proxy.</p>
|
||||
|
||||
<p>🦀Crabs🦀 and 🎂Cakes🎂 for everyone!</p>
|
||||
</body>
|
||||
</html>
|
54
scripts/oauth_proxy/run_proxy.sh
Executable file
54
scripts/oauth_proxy/run_proxy.sh
Executable file
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
|
||||
PROXY_VERSION="7-debian-11"
|
||||
PROXY_HTTP_PORT="10080"
|
||||
PROXY_HTTPS_PORT="10443"
|
||||
CLIENT_ID="test_oauth2"
|
||||
|
||||
# documentation for proxy settings is here: https://oauth2-proxy.github.io/oauth2-proxy/docs/configuration/overview/#environment-variables
|
||||
|
||||
# generate a cookie secret
|
||||
echo "OAUTH2_PROXY_COOKIE_SECRET=$(openssl rand -hex 16)" > envfile
|
||||
{
|
||||
echo "OAUTH2_PROXY_CLIENT_ID=${CLIENT_ID}"
|
||||
echo "OAUTH2_PROXY_CLIENT_SECRET_FILE=/opt/client.secret"
|
||||
echo "OAUTH2_PROXY_COOKIE_EXPIRE=300s"
|
||||
echo "OAUTH2_PROXY_CODE_CHALLENGE_METHOD=S256"
|
||||
echo "OAUTH2_PROXY_COOKIE_CSRF_EXPIRE=300s"
|
||||
echo "OAUTH2_PROXY_HTTP_ADDRESS=:${PROXY_HTTP_PORT}"
|
||||
echo "OAUTH2_PROXY_HTTPS_ADDRESS=:${PROXY_HTTPS_PORT}"
|
||||
echo "OAUTH2_PROXY_PROVIDER=oidc"
|
||||
echo "OAUTH2_PROXY_SCOPE=openid"
|
||||
echo "OAUTH2_PROXY_EMAIL_DOMAIN=example.com"
|
||||
echo "OAUTH2_PROXY_UPSTREAM=file://opt/index.html"
|
||||
echo "OAUTH2_PROXY_OIDC_ISSUER_URL=https://localhost:8443/oauth2/openid/${CLIENT_ID}"
|
||||
echo "OAUTH2_PROXY_SSL_INSECURE_SKIP_VERIFY=true"
|
||||
# cert things, loads the certs that we use for for the test server
|
||||
echo "OAUTH2_PROXY_TLS_CERT_FILE=/opt/cert.pem"
|
||||
echo "OAUTH2_PROXY_TLS_KEY_FILE=/opt/key.pem"
|
||||
|
||||
} >> envfile
|
||||
|
||||
if [ ! -f client.secret ]; then
|
||||
echo "The client.secret file is missing! Can't run!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$(cat client.secret)" ]; then
|
||||
echo "The client.secret file is empty! Can't run!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "#################################################################"
|
||||
echo " Starting the proxy"
|
||||
echo " Access it on https://localhost:${PROXY_HTTPS_PORT}"
|
||||
echo "#################################################################"
|
||||
|
||||
docker run --rm -it \
|
||||
--env-file envfile \
|
||||
--network host \
|
||||
--mount "type=bind,source=/tmp/kanidm/cert.pem,target=/opt/cert.pem" \
|
||||
--mount "type=bind,source=/tmp/kanidm/key.pem,target=/opt/key.pem" \
|
||||
--mount "type=bind,source=./index.html,target=/opt/index.html" \
|
||||
--mount "type=bind,source=./client.secret,target=/opt/client.secret" \
|
||||
"bitnami/oauth2-proxy:${PROXY_VERSION}" --email-domain='*'
|
|
@ -24,8 +24,12 @@ if [ "${1}" == "--help" ]; then
|
|||
exit 0
|
||||
fi
|
||||
if [ ! -f run_insecure_dev_server.sh ]; then
|
||||
echo "Please run from the server/daemon dir!"
|
||||
exit 1
|
||||
if [ "$(basename "$(pwd)")" == "kanidm" ]; then
|
||||
cd server/daemon || exit 1
|
||||
else
|
||||
echo "Please run from the server/daemon dir, I can't tell where you are..."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
@ -115,22 +119,32 @@ ${KANIDM} group add-members "${TEST_GROUP}" "${TEST_USER_NAME}" -D idm_admin
|
|||
echo "Enable experimental UI for admin idm_admin ${TEST_USER_NAME}"
|
||||
${KANIDM} group add-members idm_ui_enable_experimental_features admin idm_admin "${TEST_USER_NAME}" -D idm_admin
|
||||
|
||||
# create oauth2 rp
|
||||
echo "Creating the OAuth2 RP"
|
||||
${KANIDM} system oauth2 create "${OAUTH2_RP_ID}" "${OAUTH2_RP_DISPLAY}" "https://kanidm.com" -D admin
|
||||
# create oauth2 rp for kanidm.com
|
||||
echo "Creating the kanidm.com OAuth2 RP"
|
||||
${KANIDM} system oauth2 create "kanidm_com" "Kanidm.com" "https://kanidm.com" -D admin
|
||||
echo "Creating the kanidm.com OAuth2 RP Scope Map"
|
||||
${KANIDM} system oauth2 update-scope-map "kanidm_com" "${TEST_GROUP}" openid -D admin
|
||||
echo "Creating the kanidm.com OAuth2 RP Supplemental Scope Map"
|
||||
${KANIDM} system oauth2 update-sup-scope-map "kanidm_com" "${TEST_GROUP}" admin -D admin
|
||||
|
||||
echo "Creating the OAuth2 RP Scope Map"
|
||||
|
||||
# create oauth2 rp for localhost:10443 - for oauth2 proxy testing
|
||||
echo "Creating the ${OAUTH2_RP_ID} OAuth2 RP"
|
||||
${KANIDM} system oauth2 create "${OAUTH2_RP_ID}" "${OAUTH2_RP_DISPLAY}" "https://localhost:10443" -D admin
|
||||
echo "Creating the ${OAUTH2_RP_ID} OAuth2 RP Scope Map - Group ${TEST_GROUP}"
|
||||
${KANIDM} system oauth2 update-scope-map "${OAUTH2_RP_ID}" "${TEST_GROUP}" openid -D admin
|
||||
echo "Creating the OAuth2 RP Supplemental Scope Map"
|
||||
echo "Creating the ${OAUTH2_RP_ID} OAuth2 RP Supplemental Scope Map"
|
||||
${KANIDM} system oauth2 update-sup-scope-map "${OAUTH2_RP_ID}" "${TEST_GROUP}" admin -D admin
|
||||
|
||||
echo "Creating the OAuth2 RP Secondary Supplemental Crab-baite Scope Map.... wait, no that's not a thing."
|
||||
|
||||
echo "Checking the OAuth2 RP Exists"
|
||||
${KANIDM} system oauth2 list -D admin | rg -A10 "${OAUTH2_RP_ID}"
|
||||
|
||||
# config auth2
|
||||
echo "Pulling secret for the OAuth2 RP"
|
||||
${KANIDM} system oauth2 show-basic-secret -o json "${OAUTH2_RP_ID}" -D admin
|
||||
echo "Pulling secret for the ${OAUTH2_RP_ID} OAuth2 RP"
|
||||
OAUTH2_SECRET="$(${KANIDM} system oauth2 show-basic-secret -o json "${OAUTH2_RP_ID}" -D admin)"
|
||||
echo "${OAUTH2_SECRET}"
|
||||
|
||||
echo "Creating cred reset link for ${TEST_USER_NAME}"
|
||||
${KANIDM} person credential create-reset-token "${TEST_USER_NAME}" -D idm_admin
|
||||
|
@ -141,4 +155,6 @@ echo "###################################"
|
|||
echo "admin password: ${ADMIN_PASS}"
|
||||
echo "idm_admin password: ${IDM_ADMIN_PASS}"
|
||||
echo "UI URL: ${KANIDM_URL}"
|
||||
echo "OAuth2 RP ID: ${OAUTH2_RP_ID}"
|
||||
echo "OAuth2 Secret: $(echo "${OAUTH2_SECRET}" | jq -r .secret)"
|
||||
echo "###################################"
|
||||
|
|
|
@ -29,6 +29,7 @@ cron = { workspace = true }
|
|||
filetime = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
futures-util = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
http = { workspace = true }
|
||||
hyper = { workspace = true }
|
||||
kanidm_proto = { workspace = true }
|
||||
|
|
|
@ -43,15 +43,15 @@ pub struct JavaScriptFile {
|
|||
impl JavaScriptFile {
|
||||
/// returns a `<script>` HTML tag
|
||||
pub fn as_tag(&self) -> String {
|
||||
let typeattr = match &self.filetype {
|
||||
let filetype = match &self.filetype {
|
||||
Some(val) => {
|
||||
format!(" type=\"{}\"", val.as_str())
|
||||
}
|
||||
_ => String::from(""),
|
||||
};
|
||||
format!(
|
||||
r#"<script src="/pkg/{}" integrity="sha384-{}"{}></script>"#,
|
||||
self.filepath, &self.hash, &typeattr,
|
||||
r#"<script async src="/pkg/{}" integrity="sha384-{}"{}></script>"#,
|
||||
self.filepath, &self.hash, &filetype,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ pub async fn dont_cache_me<B>(request: Request<B>, next: Next<B>) -> Response {
|
|||
response
|
||||
}
|
||||
|
||||
/// Adds `no-cache max-age=0` to the response headers.
|
||||
/// Adds a cache control header of 300 seconds to the response headers.
|
||||
pub async fn cache_me<B>(request: Request<B>, next: Next<B>) -> Response {
|
||||
let mut response = next.run(request).await;
|
||||
let cache_header = CacheControl::new()
|
||||
|
|
|
@ -26,6 +26,7 @@ use axum::Router;
|
|||
use axum_csp::{CspDirectiveType, CspValue};
|
||||
use axum_macros::FromRef;
|
||||
use compact_jwt::{Jws, JwsSigner, JwsUnverified};
|
||||
use hashbrown::HashMap;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use hyper::server::accept::Accept;
|
||||
use hyper::server::conn::{AddrStream, Http};
|
||||
|
@ -62,7 +63,7 @@ pub struct ServerState {
|
|||
pub jws_signer: compact_jwt::JwsSigner,
|
||||
pub jws_validator: compact_jwt::JwsValidator,
|
||||
// The SHA384 hashes of javascript files we're going to serve to users
|
||||
pub js_files: Vec<JavaScriptFile>,
|
||||
pub js_files: JavaScriptFiles,
|
||||
pub(crate) trust_x_forward_for: bool,
|
||||
pub csp_header: HeaderValue,
|
||||
}
|
||||
|
@ -90,51 +91,80 @@ impl ServerState {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn get_js_files(role: ServerRole) -> Vec<JavaScriptFile> {
|
||||
let mut js_files: Vec<JavaScriptFile> = Vec::new();
|
||||
#[derive(Clone)]
|
||||
pub struct JavaScriptFiles {
|
||||
all_pages: Vec<JavaScriptFile>,
|
||||
selected: HashMap<String, JavaScriptFile>,
|
||||
}
|
||||
|
||||
pub fn get_js_files(role: ServerRole) -> Result<JavaScriptFiles, ()> {
|
||||
let mut all_pages: Vec<JavaScriptFile> = Vec::new();
|
||||
let mut selected: HashMap<String, JavaScriptFile> = HashMap::new();
|
||||
|
||||
if !matches!(role, ServerRole::WriteReplicaNoUI) {
|
||||
// let's set up the list of js module hashes
|
||||
{
|
||||
let filepath = "wasmloader.js";
|
||||
for filepath in [
|
||||
"wasmloader_admin.js",
|
||||
"wasmloader_login_flows.js",
|
||||
"wasmloader_user.js",
|
||||
] {
|
||||
match generate_integrity_hash(format!(
|
||||
"{}/{}",
|
||||
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
||||
filepath,
|
||||
)) {
|
||||
Ok(hash) => js_files.push(JavaScriptFile {
|
||||
filepath,
|
||||
hash,
|
||||
filetype: Some("module".to_string()),
|
||||
}),
|
||||
Ok(hash) => {
|
||||
selected.insert(
|
||||
filepath.to_string(),
|
||||
JavaScriptFile {
|
||||
filepath,
|
||||
hash,
|
||||
filetype: Some("module".to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
admin_error!(?err, "Failed to generate integrity hash for wasmloader.js")
|
||||
admin_error!(
|
||||
?err,
|
||||
"Failed to generate integrity hash for {} - cancelling startup!",
|
||||
filepath
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
};
|
||||
}
|
||||
// let's set up the list of non-module hashes
|
||||
{
|
||||
let filepath = "external/bootstrap.bundle.min.js";
|
||||
|
||||
for (filepath, filetype) in [
|
||||
("shared.js", Some("module".to_string())),
|
||||
("external/bootstrap.bundle.min.js", None),
|
||||
] {
|
||||
// let's set up the list of non-wasm-module js files we want to serve
|
||||
// for filepath in ["external/bootstrap.bundle.min.js", "shared.js"] {
|
||||
match generate_integrity_hash(format!(
|
||||
"{}/{}",
|
||||
env!("KANIDM_WEB_UI_PKG_PATH").to_owned(),
|
||||
filepath,
|
||||
)) {
|
||||
Ok(hash) => js_files.push(JavaScriptFile {
|
||||
Ok(hash) => all_pages.push(JavaScriptFile {
|
||||
filepath,
|
||||
hash,
|
||||
filetype: None,
|
||||
filetype,
|
||||
}),
|
||||
Err(err) => {
|
||||
admin_error!(
|
||||
?err,
|
||||
"Failed to generate integrity hash for bootstrap.bundle.min.js"
|
||||
)
|
||||
"Failed to generate integrity hash for {} - cancelling startup!",
|
||||
filepath
|
||||
);
|
||||
return Err(());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
js_files
|
||||
}
|
||||
Ok(JavaScriptFiles {
|
||||
all_pages,
|
||||
selected,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_https_server(
|
||||
|
@ -149,14 +179,18 @@ pub async fn create_https_server(
|
|||
error!(?e, "Failed to get jws validator");
|
||||
})?;
|
||||
|
||||
let js_files = get_js_files(config.role);
|
||||
let js_files = get_js_files(config.role)?;
|
||||
// set up the CSP headers
|
||||
// script-src 'self'
|
||||
// 'sha384-Zao7ExRXVZOJobzS/uMp0P1jtJz3TTqJU4nYXkdmsjpiVD+/wcwCyX7FGqRIqvIz'
|
||||
// 'sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM'
|
||||
// 'unsafe-eval';
|
||||
let js_directives = js_files
|
||||
.clone()
|
||||
let mut all_js_files = js_files.all_pages.clone();
|
||||
for (_, jsfile) in js_files.selected.clone() {
|
||||
all_js_files.push(jsfile);
|
||||
}
|
||||
|
||||
let js_directives = all_js_files
|
||||
.into_iter()
|
||||
.map(|f| f.hash)
|
||||
.collect::<Vec<String>>();
|
||||
|
@ -207,12 +241,18 @@ pub async fn create_https_server(
|
|||
// Create a spa router that captures everything at ui without key extraction.
|
||||
|
||||
Router::new()
|
||||
// direct users to the base app page. If a login is required,
|
||||
// then views will take care of redirection. We shouldn't redir
|
||||
// to login because that force clears previous sessions!
|
||||
// Direct users to the base app page. If a login is required,
|
||||
// then views will take care of redirection.
|
||||
.route("/", get(|| async { Redirect::temporary("/ui") }))
|
||||
.route("/manifest.webmanifest", get(manifest::manifest)) // skip_route_check
|
||||
.nest("/ui", ui::spa_router())
|
||||
// user UI app is the catch-all
|
||||
.nest("/ui", ui::spa_router_user_ui())
|
||||
// login flows app
|
||||
.nest("/ui/login", ui::spa_router_login_flows())
|
||||
.nest("/ui/reauth", ui::spa_router_login_flows())
|
||||
.nest("/ui/oauth2", ui::spa_router_login_flows())
|
||||
// admin app
|
||||
.nest("/ui/admin", ui::spa_router_admin())
|
||||
.layer(middleware::compression::new())
|
||||
.route("/ui/images/oauth2/:rs_name", get(oauth2::oauth2_image_get))
|
||||
// skip_route_check
|
||||
|
@ -238,7 +278,8 @@ pub async fn create_https_server(
|
|||
}
|
||||
let pkg_router = Router::new()
|
||||
.nest_service("/pkg", ServeDir::new(pkg_path).precompressed_br())
|
||||
.layer(middleware::compression::new());
|
||||
.layer(middleware::compression::new())
|
||||
.layer(from_fn(middleware::caching::cache_me));
|
||||
app.merge(pkg_router)
|
||||
}
|
||||
};
|
||||
|
|
|
@ -14,6 +14,9 @@ use http::header::{
|
|||
};
|
||||
use http::{HeaderMap, HeaderValue, StatusCode};
|
||||
use hyper::Body;
|
||||
use kanidm_proto::constants::uri::{
|
||||
OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT, OAUTH2_AUTHORISE_REJECT,
|
||||
};
|
||||
use kanidm_proto::constants::APPLICATION_JSON;
|
||||
use kanidm_proto::oauth2::{AccessTokenResponse, AuthorisationResponse, OidcDiscoveryResponse};
|
||||
use kanidmd_lib::idm::oauth2::{
|
||||
|
@ -742,19 +745,19 @@ pub fn route_setup(state: ServerState) -> Router<ServerState> {
|
|||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
.route(
|
||||
"/oauth2/authorise",
|
||||
OAUTH2_AUTHORISE,
|
||||
post(oauth2_authorise_post).get(oauth2_authorise_get),
|
||||
)
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
.route(
|
||||
"/oauth2/authorise/permit",
|
||||
OAUTH2_AUTHORISE_PERMIT,
|
||||
post(oauth2_authorise_permit_post).get(oauth2_authorise_permit_get),
|
||||
)
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
// IF YOU CHANGE THESE VALUES YOU MUST UPDATE OIDC DISCOVERY URLS
|
||||
.route(
|
||||
"/oauth2/authorise/reject",
|
||||
OAUTH2_AUTHORISE_REJECT,
|
||||
post(oauth2_authorise_reject_post).get(oauth2_authorise_reject_get),
|
||||
)
|
||||
// ⚠️ ⚠️ WARNING ⚠️ ⚠️
|
||||
|
|
|
@ -3,21 +3,21 @@ fn test_javscriptfile() {
|
|||
// make sure it outputs what we think it does
|
||||
use crate::https::JavaScriptFile;
|
||||
let jsf = JavaScriptFile {
|
||||
filepath: "wasmloader.js",
|
||||
filepath: "wasmloader_admin.js",
|
||||
hash: "1234567890".to_string(),
|
||||
filetype: Some("module".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
jsf.as_tag(),
|
||||
r#"<script src="/pkg/wasmloader.js" integrity="sha384-1234567890" type="module"></script>"#
|
||||
r#"<script async src="/pkg/wasmloader_admin.js" integrity="sha384-1234567890" type="module"></script>"#
|
||||
);
|
||||
let jsf = JavaScriptFile {
|
||||
filepath: "wasmloader.js",
|
||||
filepath: "wasmloader_admin.js",
|
||||
hash: "1234567890".to_string(),
|
||||
filetype: None,
|
||||
};
|
||||
assert_eq!(
|
||||
jsf.as_tag(),
|
||||
r#"<script src="/pkg/wasmloader.js" integrity="sha384-1234567890"></script>"#
|
||||
r#"<script async src="/pkg/wasmloader_admin.js" integrity="sha384-1234567890"></script>"#
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,58 +8,72 @@ use http::header::CONTENT_TYPE;
|
|||
use super::middleware::KOpId;
|
||||
use super::ServerState;
|
||||
|
||||
pub(crate) fn spa_router() -> Router<ServerState> {
|
||||
pub(crate) fn spa_router_user_ui() -> Router<ServerState> {
|
||||
Router::new()
|
||||
.route("/", get(ui_handler))
|
||||
.fallback(ui_handler)
|
||||
.route("/", get(ui_handler_user_ui))
|
||||
.fallback(ui_handler_user_ui)
|
||||
}
|
||||
|
||||
pub(crate) async fn ui_handler(
|
||||
/// This handles /ui/admin and all sub-paths
|
||||
pub(crate) fn spa_router_admin() -> Router<ServerState> {
|
||||
Router::new()
|
||||
.route("/", get(ui_handler_admin))
|
||||
.fallback(ui_handler_admin)
|
||||
}
|
||||
|
||||
/// This handles the following base paths:
|
||||
/// - /ui/login
|
||||
/// - /ui/reauth
|
||||
/// - /ui/oauth2
|
||||
pub(crate) fn spa_router_login_flows() -> Router<ServerState> {
|
||||
Router::new()
|
||||
.route("/", get(ui_handler_login_flows))
|
||||
.fallback(ui_handler_login_flows)
|
||||
}
|
||||
|
||||
pub(crate) async fn ui_handler_user_ui(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> Response<String> {
|
||||
ui_handler_generic(state, kopid, "wasmloader_user.js").await
|
||||
}
|
||||
|
||||
pub(crate) async fn ui_handler_admin(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> Response<String> {
|
||||
ui_handler_generic(state, kopid, "wasmloader_admin.js").await
|
||||
}
|
||||
|
||||
pub(crate) async fn ui_handler_login_flows(
|
||||
State(state): State<ServerState>,
|
||||
Extension(kopid): Extension<KOpId>,
|
||||
) -> Response<String> {
|
||||
ui_handler_generic(state, kopid, "wasmloader_login_flows.js").await
|
||||
}
|
||||
|
||||
pub(crate) async fn ui_handler_generic(
|
||||
state: ServerState,
|
||||
kopid: KOpId,
|
||||
wasmloader: &str,
|
||||
) -> Response<String> {
|
||||
let domain_display_name = state.qe_r_ref.get_domain_display_name(kopid.eventid).await;
|
||||
|
||||
// this feels icky but I felt that adding a trait on Vec<JavaScriptFile> which generated the string was going a bit far
|
||||
let jsfiles: Vec<String> = state.js_files.into_iter().map(|j| j.as_tag()).collect();
|
||||
let jstags = jsfiles.join(" ");
|
||||
// let's get the tags we want to load the javascript files
|
||||
let mut jsfiles: Vec<String> = state
|
||||
.js_files
|
||||
.all_pages
|
||||
.into_iter()
|
||||
.map(|j| j.as_tag())
|
||||
.collect();
|
||||
if let Some(jsfile) = state.js_files.selected.get(wasmloader) {
|
||||
jsfiles.push(jsfile.clone().as_tag())
|
||||
};
|
||||
|
||||
let jstags = jsfiles.join("\n");
|
||||
|
||||
let body = format!(
|
||||
r#"
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="theme-color" content="white" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{}</title>
|
||||
|
||||
<link rel="icon" href="/pkg/img/favicon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="/pkg/img/logo-256.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/pkg/img/logo-180.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/pkg/img/logo-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/pkg/img/logo-square.svg" />
|
||||
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"/>
|
||||
<link rel="stylesheet" href="/pkg/style.css"/>
|
||||
|
||||
{}
|
||||
|
||||
</head>
|
||||
<body class="flex-column d-flex h-100">
|
||||
<main class="flex-shrink-0 form-signin">
|
||||
<center>
|
||||
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
|
||||
<h3>Kanidm is loading, please wait... </h3>
|
||||
</center>
|
||||
</main>
|
||||
<footer class="footer mt-auto py-3 bg-light text-end">
|
||||
<div class="container">
|
||||
<span class="text-muted">Powered by <a href="https://kanidm.com">Kanidm</a></span>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>"#,
|
||||
include_str!("ui_html.html"),
|
||||
domain_display_name.as_str(),
|
||||
jstags,
|
||||
);
|
||||
|
|
40
server/core/src/https/ui_html.html
Normal file
40
server/core/src/https/ui_html.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="theme-color" content="white" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>{}</title>
|
||||
|
||||
<link rel="icon" href="/pkg/img/favicon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" href="/pkg/img/logo-256.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180"
|
||||
href="/pkg/img/logo-180.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192"
|
||||
href="/pkg/img/logo-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512"
|
||||
href="/pkg/img/logo-square.svg" />
|
||||
<link rel="stylesheet" href="/pkg/external/bootstrap.min.css"
|
||||
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" />
|
||||
<link rel="stylesheet" href="/pkg/style.css" />
|
||||
|
||||
{}
|
||||
|
||||
</head>
|
||||
<body class="flex-column d-flex h-100">
|
||||
<main class="flex-shrink-0 form-signin">
|
||||
<center>
|
||||
<img src="/pkg/img/logo-square.svg" alt="Kanidm"
|
||||
class="kanidm_logo" />
|
||||
<h3>Kanidm is loading, please wait... </h3>
|
||||
</center>
|
||||
</main>
|
||||
<footer class="footer mt-auto py-3 bg-light text-end">
|
||||
<div class="container">
|
||||
<span class="text-muted">Powered by <a href="https://kanidm.com">Kanidm</a></span>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
|
@ -9,6 +9,7 @@ use axum::routing::{delete, get, post, put};
|
|||
use axum::{Extension, Json, Router};
|
||||
use compact_jwt::Jws;
|
||||
use http::{HeaderMap, HeaderValue};
|
||||
use kanidm_proto::constants::uri::V1_AUTH_VALID;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -3121,7 +3122,7 @@ pub(crate) fn route_setup(state: ServerState) -> Router<ServerState> {
|
|||
// get(|| async { "TODO" }),
|
||||
// )
|
||||
.route("/v1/auth", post(auth))
|
||||
.route("/v1/auth/valid", get(auth_valid))
|
||||
.route(V1_AUTH_VALID, get(auth_valid))
|
||||
.route("/v1/logout", get(logout))
|
||||
.route("/v1/reauth", post(reauth))
|
||||
.with_state(state.clone())
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "kanidmd_lib_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = { workspace = true }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
@ -12,4 +12,3 @@ doctest = false
|
|||
proc-macro2 = { workspace = true }
|
||||
quote = { workspace = true }
|
||||
syn = { workspace = true }
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "testkit-macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = { workspace = true }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
|
|
@ -41,7 +41,7 @@ tracing = { workspace = true, features = ["attributes"] }
|
|||
tokio = { workspace = true, features = ["net", "sync", "io-util", "macros"] }
|
||||
openssl = { workspace = true }
|
||||
lazy_static = { workspace = true }
|
||||
petgraph = { version = "0.6.4", features = ["serde"] }
|
||||
petgraph = { version = "0.6.4", features = ["serde", "serde-1"] }
|
||||
regex.workspace = true
|
||||
|
||||
|
||||
|
|
|
@ -8,13 +8,97 @@ use std::collections::{BTreeSet, HashMap};
|
|||
use kanidmd_lib::constants::entries::Attribute;
|
||||
use kanidmd_lib::constants::groups::{idm_builtin_admin_groups, idm_builtin_non_admin_groups};
|
||||
use kanidmd_lib::prelude::{builtin_accounts, EntryInitNew};
|
||||
use petgraph::graphmap::GraphMap;
|
||||
use petgraph::graphmap::{AllEdges, GraphMap, NodeTrait};
|
||||
use petgraph::Directed;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
enum EdgeType {
|
||||
MemberOf,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
enum EntryType {
|
||||
Person(String),
|
||||
ServiceAccount(String),
|
||||
Group(String),
|
||||
UnknownType(String),
|
||||
}
|
||||
|
||||
impl EntryType {
|
||||
fn as_mermaid_tag(&self) -> String {
|
||||
match self {
|
||||
EntryType::Person(name) => format!("{}(\"Person:{}\")", name, name),
|
||||
EntryType::ServiceAccount(name) => format!("{}{{\"SA: {}\"}}", name, name),
|
||||
EntryType::Group(name) => format!("{}[\"Group: {}\"]", name, name),
|
||||
EntryType::UnknownType(name) => format!("{}[\"Unknown Type {}\"]", name, name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EntryInitNew> for EntryType {
|
||||
fn from(entry: &EntryInitNew) -> Self {
|
||||
let name = entry.get_ava_single(Attribute::Name).unwrap();
|
||||
let name = name.as_string().unwrap();
|
||||
let classes = entry
|
||||
.get_ava_set(Attribute::Class)
|
||||
.unwrap()
|
||||
.as_iutf8_set()
|
||||
.cloned()
|
||||
.unwrap_or(BTreeSet::<String>::new());
|
||||
if classes.contains("group") {
|
||||
EntryType::Group(name.clone())
|
||||
} else if classes.contains("service_account") {
|
||||
EntryType::ServiceAccount(name.clone())
|
||||
} else if classes.contains("person") {
|
||||
EntryType::Person(name.clone())
|
||||
} else {
|
||||
EntryType::UnknownType(name.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Graph<T>(GraphMap<T, EdgeType, petgraph::Directed>);
|
||||
|
||||
impl<T> Graph<T>
|
||||
where
|
||||
T: core::hash::Hash + Ord + NodeTrait,
|
||||
{
|
||||
fn new() -> Self {
|
||||
Graph(GraphMap::<T, EdgeType, petgraph::Directed>::new())
|
||||
}
|
||||
|
||||
fn add_node(&mut self, n: T) {
|
||||
self.0.add_node(n);
|
||||
}
|
||||
fn add_edge(&mut self, l: T, r: T, t: EdgeType) {
|
||||
self.0.add_edge(l, r, t);
|
||||
}
|
||||
fn all_edges(&mut self) -> AllEdges<'_, T, EdgeType, Directed> {
|
||||
self.0.all_edges()
|
||||
}
|
||||
|
||||
/// The uuidmap is a map of uuids to EntryInitNew objects, which we use to get the name of the objects
|
||||
fn as_mermaid(&mut self, uuidmap: &HashMap<T, EntryInitNew>) -> String {
|
||||
let mut res = format!("graph RL;\n");
|
||||
for (left, right, _weight) in self.all_edges() {
|
||||
let left = uuidmap.get(&left).unwrap();
|
||||
let right = uuidmap.get(&right).unwrap();
|
||||
|
||||
res += &format!(
|
||||
" {} --> {}\n",
|
||||
EntryType::from(left).as_mermaid_tag(),
|
||||
EntryType::from(right).as_mermaid_tag(),
|
||||
);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
async fn enumerate_default_groups(/*_client: KanidmClient*/) {
|
||||
let mut uuidmap: HashMap<Uuid, EntryInitNew> = HashMap::new();
|
||||
|
||||
let mut graph = GraphMap::<Uuid, (), petgraph::Undirected>::new();
|
||||
let mut graph = Graph::new();
|
||||
|
||||
builtin_accounts().into_iter().for_each(|account| {
|
||||
// println!("adding builtin {}", account.uuid);
|
||||
|
@ -22,95 +106,20 @@ async fn enumerate_default_groups(/*_client: KanidmClient*/) {
|
|||
graph.add_node(account.uuid);
|
||||
});
|
||||
|
||||
idm_builtin_non_admin_groups()
|
||||
.into_iter()
|
||||
.for_each(|group| {
|
||||
uuidmap.insert(group.uuid, group.clone().try_into().unwrap());
|
||||
graph.add_node(group.uuid);
|
||||
let mut groups = idm_builtin_admin_groups();
|
||||
groups.extend(idm_builtin_non_admin_groups());
|
||||
|
||||
group.members.iter().for_each(|member| {
|
||||
graph.add_edge(*member, group.uuid, ());
|
||||
});
|
||||
});
|
||||
|
||||
idm_builtin_admin_groups().into_iter().for_each(|group| {
|
||||
groups.into_iter().for_each(|group| {
|
||||
uuidmap.insert(group.uuid, group.clone().try_into().unwrap());
|
||||
graph.add_node(group.uuid);
|
||||
|
||||
// handle the membership
|
||||
group.members.iter().for_each(|member| {
|
||||
graph.add_edge(*member, group.uuid, ());
|
||||
graph.add_edge(*member, group.uuid, EdgeType::MemberOf);
|
||||
});
|
||||
});
|
||||
|
||||
// // println!("{}", mermaidchart);
|
||||
// let mut dotgraph = format!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
|
||||
// // regex to extract uuids
|
||||
// // let re = regex::Regex::new(r"(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})").unwrap();
|
||||
// for (uuid, uuid_value) in uuidmap.clone() {
|
||||
// let uuid_str = uuid.to_string();
|
||||
// if dotgraph.contains(&uuid_str) {
|
||||
// // println!("uuid {} not found in graph", uuid_str);
|
||||
// let name = uuid_value.get_ava_single(Attribute::Name).unwrap();
|
||||
// dotgraph = dotgraph.replace(&uuid_str, name.as_string().unwrap());
|
||||
// }
|
||||
// }
|
||||
// // println!("{}", dotgraph);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum EntryType {
|
||||
Person(String),
|
||||
ServiceAccount(String),
|
||||
Group(String),
|
||||
UnknownType(String),
|
||||
}
|
||||
|
||||
impl EntryType {
|
||||
fn as_mermaid_tag(&self) -> String {
|
||||
match self {
|
||||
EntryType::Person(name) => format!("{}(\"Person:{}\")", name, name),
|
||||
EntryType::ServiceAccount(name) => format!("{}{{\"SA: {}\"}}", name, name),
|
||||
EntryType::Group(name) => format!("{}[\"Group: {}\"]", name, name),
|
||||
EntryType::UnknownType(name) => format!("{}[\"Unknown Type {}\"]", name, name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EntryInitNew> for EntryType {
|
||||
fn from(entry: EntryInitNew) -> Self {
|
||||
let name = entry.get_ava_single(Attribute::Name).unwrap();
|
||||
let name = name.as_string().unwrap();
|
||||
let classes = entry
|
||||
.get_ava_set(Attribute::Class)
|
||||
.unwrap()
|
||||
.as_iutf8_set()
|
||||
.cloned()
|
||||
.unwrap_or(BTreeSet::<String>::new());
|
||||
if classes.contains("group") {
|
||||
EntryType::Group(name.clone())
|
||||
} else if classes.contains("service_account") {
|
||||
EntryType::ServiceAccount(name.clone())
|
||||
} else if classes.contains("person") {
|
||||
EntryType::Person(name.clone())
|
||||
} else {
|
||||
EntryType::UnknownType(name.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("graph RL;");
|
||||
for (left, right, _weight) in graph.all_edges() {
|
||||
let left = uuidmap.get(&left).unwrap();
|
||||
// let left_name = left.get_ava_single(Attribute::Name).unwrap();
|
||||
|
||||
let right = uuidmap.get(&right).unwrap();
|
||||
// let right_name = right.get_ava_single(Attribute::Name).unwrap();
|
||||
|
||||
println!(
|
||||
" {} --> {}",
|
||||
EntryType::from(left.clone()).as_mermaid_tag(),
|
||||
EntryType::from(right.clone()).as_mermaid_tag(),
|
||||
);
|
||||
}
|
||||
println!("{}", graph.as_mermaid(&uuidmap))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::convert::TryFrom;
|
|||
use std::str::FromStr;
|
||||
|
||||
use compact_jwt::{JwkKeySet, JwsValidator, OidcToken, OidcUnverified};
|
||||
use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT};
|
||||
use kanidm_proto::constants::*;
|
||||
use kanidm_proto::oauth2::{
|
||||
AccessTokenIntrospectRequest, AccessTokenIntrospectResponse, AccessTokenRequest,
|
||||
|
@ -221,7 +222,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
|||
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
let response = client
|
||||
.get(rsclient.make_url("/oauth2/authorise"))
|
||||
.get(rsclient.make_url(OAUTH2_AUTHORISE))
|
||||
.bearer_auth(oauth_test_uat.clone())
|
||||
.query(&[
|
||||
("response_type", "code"),
|
||||
|
@ -261,7 +262,7 @@ async fn test_oauth2_openid_basic_flow(rsclient: KanidmClient) {
|
|||
// state and code.
|
||||
|
||||
let response = client
|
||||
.get(rsclient.make_url("/oauth2/authorise/permit"))
|
||||
.get(rsclient.make_url(OAUTH2_AUTHORISE_PERMIT))
|
||||
.bearer_auth(oauth_test_uat)
|
||||
.query(&[("token", consent_token.as_str())])
|
||||
.send()
|
||||
|
@ -521,7 +522,7 @@ async fn test_oauth2_openid_public_flow(rsclient: KanidmClient) {
|
|||
let (pkce_code_challenge, pkce_code_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
let response = client
|
||||
.get(rsclient.make_url("/oauth2/authorise"))
|
||||
.get(rsclient.make_url(OAUTH2_AUTHORISE))
|
||||
.bearer_auth(oauth_test_uat.clone())
|
||||
.query(&[
|
||||
("response_type", "code"),
|
||||
|
@ -560,7 +561,7 @@ async fn test_oauth2_openid_public_flow(rsclient: KanidmClient) {
|
|||
// Step 2 - we now send the consent get to the server which yields a redirect with a
|
||||
// state and code.
|
||||
let response = client
|
||||
.get(rsclient.make_url("/oauth2/authorise/permit"))
|
||||
.get(rsclient.make_url(OAUTH2_AUTHORISE_PERMIT))
|
||||
.bearer_auth(oauth_test_uat)
|
||||
.query(&[("token", consent_token.as_str())])
|
||||
.send()
|
||||
|
|
70
server/web_ui/admin/Cargo.toml
Normal file
70
server/web_ui/admin/Cargo.toml
Normal file
|
@ -0,0 +1,70 @@
|
|||
[package]
|
||||
name = "kanidmd_web_ui_admin"
|
||||
description = "Kanidm Server Web UI - Admin Interface"
|
||||
documentation = "https://docs.rs/kanidm/latest/kanidm/"
|
||||
|
||||
version = { workspace = true }
|
||||
authors = [
|
||||
"William Brown <william@blackhats.net.au>",
|
||||
"James Hodgkinson <james@terminaloutcomes.com>",
|
||||
]
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
kanidm_proto = { workspace = true, features = ["wasm"] }
|
||||
kanidmd_web_ui_shared = { workspace = true }
|
||||
gloo = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
lazy_static.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde-wasm-bindgen = { workspace = true }
|
||||
time = { workspace = true }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
yew = { workspace = true, features = ["csr"] }
|
||||
yew-router = { workspace = true }
|
||||
gloo-utils = { workspace = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
workspace = true
|
||||
features = [
|
||||
# "AuthenticationExtensionsClientOutputs",
|
||||
# "AuthenticatorResponse",
|
||||
# "CredentialCreationOptions",
|
||||
# "CredentialRequestOptions",
|
||||
# "CredentialsContainer",
|
||||
"DomTokenList",
|
||||
"Element",
|
||||
"Event",
|
||||
"FocusEvent",
|
||||
"FormData",
|
||||
"Headers",
|
||||
"HtmlButtonElement",
|
||||
"HtmlDocument",
|
||||
"HtmlFormElement",
|
||||
"Navigator",
|
||||
# "PublicKeyCredential",
|
||||
# "PublicKeyCredentialCreationOptions",
|
||||
# "PublicKeyCredentialRpEntity",
|
||||
# "PublicKeyCredentialUserEntity",
|
||||
"Request",
|
||||
# "RequestCredentials",
|
||||
# "RequestInit",
|
||||
# "RequestMode",
|
||||
# "RequestRedirect",
|
||||
"Response",
|
||||
"Window",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = { workspace = true }
|
2
server/web_ui/admin/build_dev.sh
Executable file
2
server/web_ui/admin/build_dev.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
BUILD_FLAGS="--dev" ./build.sh
|
1033
server/web_ui/admin/pkg/kanidmd_web_ui_admin.js
Normal file
1033
server/web_ui/admin/pkg/kanidmd_web_ui_admin.js
Normal file
File diff suppressed because it is too large
Load diff
BIN
server/web_ui/admin/pkg/kanidmd_web_ui_admin_bg.wasm
Normal file
BIN
server/web_ui/admin/pkg/kanidmd_web_ui_admin_bg.wasm
Normal file
Binary file not shown.
7
server/web_ui/admin/pkg/wasmloader_admin.js
Normal file
7
server/web_ui/admin/pkg/wasmloader_admin.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// loads the module which loads the WASM. It's loaders all the way down.
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui_admin.js';
|
||||
async function main() {
|
||||
await init('/pkg/kanidmd_web_ui_admin_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
|
@ -4,14 +4,10 @@ use gloo::console;
|
|||
use yew::{html, Component, Context, Html, Properties};
|
||||
use yew_router::prelude::Link;
|
||||
|
||||
use super::prelude::*;
|
||||
use crate::components::admin_menu::{Entity, EntityType, GetError};
|
||||
use crate::components::alpha_warning_banner;
|
||||
use crate::constants::{
|
||||
CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_DT, CSS_TABLE,
|
||||
};
|
||||
use crate::utils::{do_alert_error, do_page_header};
|
||||
use crate::views::AdminRoute;
|
||||
use crate::{do_request, RequestMethod};
|
||||
use crate::router::AdminRoute;
|
||||
use kanidmd_web_ui_shared::constants::{CSS_CELL, CSS_DT, CSS_TABLE};
|
||||
|
||||
impl From<GetError> for AdminListAccountsMsg {
|
||||
fn from(ge: GetError) -> Self {
|
||||
|
@ -185,11 +181,6 @@ impl Component for AdminListAccounts {
|
|||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
html! {
|
||||
<>
|
||||
|
||||
<ol class="breadcrumb">
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{"Accounts"}</li>
|
||||
</ol>
|
||||
{do_page_header("Account Administration")}
|
||||
{ alpha_warning_banner() }
|
||||
<div id={"accountlist"}>
|
||||
|
@ -269,12 +260,12 @@ impl Component for AdminListAccounts {
|
|||
console::error!("Failed to pull details", format!("{:?}", kopid));
|
||||
html!(
|
||||
<>
|
||||
{do_alert_error("Failed to Query Accounts", Some(emsg))}
|
||||
{do_alert_error("Failed to Query Accounts", Some(emsg), false)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
ViewState::NotAuthorized {} => {
|
||||
do_alert_error("You're not authorized to see this page!", None)
|
||||
do_alert_error("You're not authorized to see this page!", None, false)
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
|
@ -353,6 +344,7 @@ impl Component for AdminViewPerson {
|
|||
ViewAccountState::Failed { emsg, kopid } => do_alert_error(
|
||||
emsg.clone().as_str(),
|
||||
Some(&format!("Operation ID: {:?}", kopid)),
|
||||
false,
|
||||
),
|
||||
// TODO: the not authorized page needs to be better
|
||||
ViewAccountState::NotAuthorized {} => {
|
||||
|
@ -381,11 +373,6 @@ impl Component for AdminViewPerson {
|
|||
// };
|
||||
html! {
|
||||
<>
|
||||
<ol class="breadcrumb">
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminListAccounts}>{"Accounts"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{username.as_str()}</li>
|
||||
</ol>
|
||||
{do_page_header(display_name.as_str())}
|
||||
{alpha_warning_banner()}
|
||||
|
||||
|
@ -504,11 +491,7 @@ impl Component for AdminViewServiceAccount {
|
|||
|
||||
html! {
|
||||
<>
|
||||
<ol class="breadcrumb">
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminListAccounts}>{"Accounts"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{username}</li>
|
||||
</ol>
|
||||
|
||||
{do_page_header(&format!("Service Account: {}", username))}
|
||||
{alpha_warning_banner()}
|
||||
<p>{"Display Name: "}{displayname}</p>
|
||||
|
@ -517,7 +500,7 @@ impl Component for AdminViewServiceAccount {
|
|||
}
|
||||
}
|
||||
ViewAccountState::Failed { emsg, kopid } => html! {
|
||||
do_alert_error(emsg.as_str(), Some(&format!("Operation ID: {:?}", kopid)))
|
||||
do_alert_error(emsg.as_str(), Some(&format!("Operation ID: {:?}", kopid)), false)
|
||||
},
|
||||
// TODO: this error needs fixing
|
||||
ViewAccountState::NotAuthorized {} => {
|
|
@ -1,15 +1,14 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use gloo::console;
|
||||
use kanidmd_web_ui_shared::utils::{do_alert_error, do_page_header};
|
||||
use yew::{html, Component, Context, Html, Properties};
|
||||
use yew_router::prelude::Link;
|
||||
|
||||
use crate::components::admin_menu::{Entity, EntityType, GetError};
|
||||
use crate::components::alpha_warning_banner;
|
||||
use crate::constants::{CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_TABLE};
|
||||
use crate::utils::{do_alert_error, do_page_header};
|
||||
use crate::views::AdminRoute;
|
||||
use crate::{do_request, RequestMethod};
|
||||
use crate::router::AdminRoute;
|
||||
use kanidmd_web_ui_shared::constants::{CSS_CELL, CSS_TABLE};
|
||||
use kanidmd_web_ui_shared::{alpha_warning_banner, do_request, RequestMethod};
|
||||
|
||||
impl From<GetError> for AdminListGroupsMsg {
|
||||
fn from(ge: GetError) -> Self {
|
||||
|
@ -224,12 +223,12 @@ impl AdminListGroups {
|
|||
console::error!("Failed to pull details", format!("{:?}", kopid));
|
||||
html!(
|
||||
<>
|
||||
{do_alert_error("Failed to Query Groups", Some(emsg))}
|
||||
{do_alert_error("Failed to Query Groups", Some(emsg), false)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
GroupsViewState::NotAuthorized {} => {
|
||||
do_alert_error("You're not authorized to see this page!", None)
|
||||
do_alert_error("You're not authorized to see this page!", None, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -318,11 +317,6 @@ impl Component for AdminViewGroup {
|
|||
};
|
||||
html! {
|
||||
<>
|
||||
<ol class="breadcrumb">
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminListGroups}>{"Groups"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{group_name}</li>
|
||||
</ol>
|
||||
{do_page_header(&page_title)}
|
||||
<p>{"UUID: "}{group_uuid}</p>
|
||||
// TODO: pull group membership and show members
|
||||
|
@ -337,9 +331,10 @@ impl Component for AdminViewGroup {
|
|||
.as_ref()
|
||||
.unwrap_or(&String::from("unknown operation ID")),
|
||||
),
|
||||
false,
|
||||
),
|
||||
GroupViewState::NotAuthorized {} => {
|
||||
do_alert_error("You are not authorized to view this page!", None)
|
||||
do_alert_error("You are not authorized to view this page!", None, false)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
use kanidmd_web_ui_shared::alpha_warning_banner;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use yew::{html, Component, Context, Html, Properties};
|
||||
use yew_router::prelude::Link;
|
||||
|
||||
use crate::components::alpha_warning_banner;
|
||||
use crate::constants::{CSS_CARD, CSS_CARD_BODY, CSS_LINK_DARK_STRETCHED, CSS_PAGE_HEADER};
|
||||
use kanidmd_web_ui_shared::constants::{
|
||||
CSS_CARD, CSS_CARD_BODY, CSS_LINK_DARK_STRETCHED, CSS_PAGE_HEADER,
|
||||
};
|
||||
// use crate::error::FetchError;
|
||||
use crate::views::AdminRoute;
|
||||
use crate::router::AdminRoute;
|
||||
|
||||
#[derive(Eq, PartialEq, Properties)]
|
||||
pub struct Props;
|
|
@ -4,12 +4,10 @@ use gloo::console;
|
|||
use yew::{html, Component, Context, Html, Properties};
|
||||
use yew_router::prelude::Link;
|
||||
|
||||
use super::prelude::*;
|
||||
use crate::components::admin_menu::{Entity, EntityType, GetError};
|
||||
use crate::components::alpha_warning_banner;
|
||||
use crate::constants::{CSS_BREADCRUMB_ITEM, CSS_BREADCRUMB_ITEM_ACTIVE, CSS_CELL, CSS_TABLE};
|
||||
use crate::utils::{do_alert_error, do_page_header};
|
||||
use crate::views::AdminRoute;
|
||||
use crate::{do_request, RequestMethod};
|
||||
use crate::router::AdminRoute;
|
||||
use kanidmd_web_ui_shared::constants::{CSS_CELL, CSS_TABLE};
|
||||
|
||||
impl From<GetError> for AdminListOAuth2Msg {
|
||||
fn from(ge: GetError) -> Self {
|
||||
|
@ -164,10 +162,6 @@ impl Component for AdminListOAuth2 {
|
|||
html! {
|
||||
<>
|
||||
|
||||
<ol class="breadcrumb">
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{"OAuth2"}</li>
|
||||
</ol>
|
||||
{do_page_header("OAuth2")}
|
||||
|
||||
{ alpha_warning_banner() }
|
||||
|
@ -237,12 +231,12 @@ impl Component for AdminListOAuth2 {
|
|||
console::error!("Failed to pull details", format!("{:?}", kopid));
|
||||
html!(
|
||||
<>
|
||||
{do_alert_error("Failed to Query OAuth2", Some(emsg))}
|
||||
{do_alert_error("Failed to Query OAuth2", Some(emsg), false)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
ListViewState::NotAuthorized {} => {
|
||||
do_alert_error("You're not authorized to see this page!", None)
|
||||
do_alert_error("You're not authorized to see this page!", None, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -350,11 +344,6 @@ impl Component for AdminViewOAuth2 {
|
|||
html! {
|
||||
<>
|
||||
|
||||
<ol class="breadcrumb">
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM}><Link<AdminRoute> to={AdminRoute::AdminListOAuth2}>{"OAuth2"}</Link<AdminRoute>></li>
|
||||
<li class={CSS_BREADCRUMB_ITEM_ACTIVE} aria-current="page">{display_name.as_str()}</li>
|
||||
</ol>
|
||||
{do_page_header(display_name.as_str())}
|
||||
{alpha_warning_banner()}
|
||||
|
||||
|
@ -369,12 +358,12 @@ impl Component for AdminViewOAuth2 {
|
|||
console::error!("Failed to pull details", format!("{:?}", kopid));
|
||||
html!(
|
||||
<>
|
||||
{do_alert_error("Failed to Query OAuth2", Some(emsg))}
|
||||
{do_alert_error("Failed to Query OAuth2", Some(emsg), false)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
ViewState::NotAuthorized {} => {
|
||||
do_alert_error("You're not authorized to see this page!", None)
|
||||
do_alert_error("You're not authorized to see this page!", None, false)
|
||||
}
|
||||
}
|
||||
}
|
11
server/web_ui/admin/src/components/mod.rs
Normal file
11
server/web_ui/admin/src/components/mod.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
pub mod admin_accounts;
|
||||
pub mod admin_groups;
|
||||
pub mod admin_menu;
|
||||
pub mod admin_oauth2;
|
||||
|
||||
mod prelude {
|
||||
|
||||
pub use kanidmd_web_ui_shared::alpha_warning_banner;
|
||||
pub use kanidmd_web_ui_shared::utils::{do_alert_error, do_page_header};
|
||||
pub use kanidmd_web_ui_shared::{do_request, RequestMethod};
|
||||
}
|
152
server/web_ui/admin/src/lib.rs
Normal file
152
server/web_ui/admin/src/lib.rs
Normal file
|
@ -0,0 +1,152 @@
|
|||
mod components;
|
||||
mod router;
|
||||
|
||||
use gloo::console::{self, error};
|
||||
use kanidmd_web_ui_shared::add_body_form_classes;
|
||||
use kanidmd_web_ui_shared::constants::{
|
||||
CSS_NAVBAR_BRAND, CSS_NAVBAR_LINKS_UL, CSS_NAVBAR_NAV, CSS_NAV_LINK, ID_NAVBAR_COLLAPSE,
|
||||
IMG_LOGO_SQUARE, URL_USER_HOME,
|
||||
};
|
||||
use kanidmd_web_ui_shared::ui::{signout_link, signout_modal, ui_logout};
|
||||
use kanidmd_web_ui_shared::utils::do_footer;
|
||||
#[allow(unused_imports)] // because it's needed to compile wasm things
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::JsValue;
|
||||
|
||||
use yew::{html, Component, Context, Html};
|
||||
use yew_router::prelude::Link;
|
||||
use yew_router::{BrowserRouter, Switch};
|
||||
|
||||
use crate::router::AdminRoute;
|
||||
|
||||
pub struct AdminApp {}
|
||||
|
||||
/// This builds the navbar, it's not generic because the link on the logo is different
|
||||
fn make_navbar(links: Vec<Html>) -> Html {
|
||||
html! {
|
||||
<nav class={CSS_NAVBAR_NAV}>
|
||||
<div class="container-fluid">
|
||||
<a href={URL_USER_HOME} class={CSS_NAVBAR_BRAND}>
|
||||
{"Kanidm Administration"}
|
||||
</a>
|
||||
// this shows a button on mobile devices to open the menu
|
||||
<button class="navbar-toggler bg-light" type="button" data-bs-toggle="collapse" data-bs-target={["#", ID_NAVBAR_COLLAPSE].concat()} aria-controls={ID_NAVBAR_COLLAPSE} aria-expanded="false" aria-label="Toggle navigation">
|
||||
<img src={IMG_LOGO_SQUARE} alt="Toggle navigation" class="navbar-toggler-img" />
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id={ID_NAVBAR_COLLAPSE}>
|
||||
<ul class={CSS_NAVBAR_LINKS_UL}>
|
||||
{ links.into_iter().map(|link| {
|
||||
html!{ <li class="mb-1">
|
||||
{ link }
|
||||
</li>
|
||||
} }).collect::<Html>()
|
||||
}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AdminViewsMsg {
|
||||
Logout,
|
||||
LogoutComplete,
|
||||
}
|
||||
|
||||
impl Component for AdminApp {
|
||||
type Message = AdminViewsMsg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("manager::create");
|
||||
AdminApp {}
|
||||
}
|
||||
|
||||
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("manager::change");
|
||||
false
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("manager::update");
|
||||
match msg {
|
||||
AdminViewsMsg::Logout => {
|
||||
console::debug!("manager::update -> logout");
|
||||
|
||||
ctx.link().send_future(async {
|
||||
match Self::fetch_logout().await {
|
||||
Ok(v) => v,
|
||||
Err(v) => {
|
||||
error!("... failed to log out? {:?}", v);
|
||||
AdminViewsMsg::Logout
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
AdminViewsMsg::LogoutComplete => {
|
||||
let window = gloo_utils::window();
|
||||
window.location().set_href(URL_USER_HOME).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("manager::rendered");
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
add_body_form_classes!();
|
||||
|
||||
let links = vec![
|
||||
html! {<a href={URL_USER_HOME} class={CSS_NAV_LINK}>{"Home"}</a>},
|
||||
html! {<Link<AdminRoute> classes={CSS_NAV_LINK} to={AdminRoute::AdminMenu}>{"Admin"}</Link<AdminRoute>>},
|
||||
html! {<Link<AdminRoute> classes={CSS_NAV_LINK} to={AdminRoute::AdminListAccounts}>{"Accounts"}</Link<AdminRoute>>},
|
||||
html! {<Link<AdminRoute> classes={CSS_NAV_LINK} to={AdminRoute::AdminListGroups}>{"Groups"}</Link<AdminRoute>>},
|
||||
html! {<Link<AdminRoute> classes={CSS_NAV_LINK} to={AdminRoute::AdminListOAuth2}>{"OAuth2"}</Link<AdminRoute>>},
|
||||
signout_link(),
|
||||
];
|
||||
|
||||
html! {
|
||||
<BrowserRouter>
|
||||
|
||||
// sign out modal dialogue box
|
||||
{signout_modal(ctx, AdminViewsMsg::Logout)}
|
||||
{make_navbar(links)}
|
||||
|
||||
<main class="p-3 x-auto">
|
||||
<Switch<AdminRoute> render={ router::switch } />
|
||||
</main>
|
||||
{ do_footer() }
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AdminApp {
|
||||
async fn fetch_logout() -> Result<AdminViewsMsg, String> {
|
||||
match ui_logout().await {
|
||||
Ok(_) => Ok(AdminViewsMsg::LogoutComplete),
|
||||
Err((emsg, _kopid)) => {
|
||||
error!("failed to process logout request: {}", emsg);
|
||||
Ok(AdminViewsMsg::Logout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the entry point of the web front end. This triggers the manager app to load and begin
|
||||
/// its event loop.
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
|
||||
pub fn run_app() -> Result<(), JsValue> {
|
||||
yew::Renderer::<AdminApp>::new().render();
|
||||
Ok(())
|
||||
}
|
68
server/web_ui/admin/src/router.rs
Normal file
68
server/web_ui/admin/src/router.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
#![allow(clippy::disallowed_types)] // because `Routable` uses a hashmap
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use yew::{html, Html};
|
||||
use yew_router::prelude::Redirect;
|
||||
use yew_router::Routable;
|
||||
|
||||
use crate::components;
|
||||
|
||||
#[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum AdminRoute {
|
||||
#[at("/ui/admin")]
|
||||
AdminMenu,
|
||||
#[at("/ui/admin/groups")]
|
||||
AdminListGroups,
|
||||
#[at("/ui/admin/accounts")]
|
||||
AdminListAccounts,
|
||||
#[at("/ui/admin/oauth2")]
|
||||
AdminListOAuth2,
|
||||
|
||||
#[at("/ui/admin/group/:uuid")]
|
||||
ViewGroup { uuid: String },
|
||||
#[at("/ui/admin/person/:uuid")]
|
||||
ViewPerson { uuid: String },
|
||||
#[at("/ui/admin/service_account/:uuid")]
|
||||
ViewServiceAccount { uuid: String },
|
||||
#[at("/ui/admin/oauth2/:rs_name")]
|
||||
ViewOAuth2RP { rs_name: String },
|
||||
|
||||
#[not_found]
|
||||
#[at("/ui/admin/404")]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
pub(crate) fn switch(route: AdminRoute) -> Html {
|
||||
match route {
|
||||
AdminRoute::AdminMenu => html! {
|
||||
<components::admin_menu::AdminMenu />
|
||||
},
|
||||
AdminRoute::AdminListAccounts => html!(
|
||||
<components::admin_accounts::AdminListAccounts />
|
||||
),
|
||||
AdminRoute::AdminListGroups => html!(
|
||||
<components::admin_groups::AdminListGroups />
|
||||
),
|
||||
AdminRoute::AdminListOAuth2 => html!(
|
||||
<components::admin_oauth2::AdminListOAuth2 />
|
||||
),
|
||||
AdminRoute::ViewGroup { uuid } => {
|
||||
html!(<components::admin_groups::AdminViewGroup uuid={uuid} />)
|
||||
// html! {<></>}
|
||||
}
|
||||
AdminRoute::ViewPerson { uuid } => html!(
|
||||
<components::admin_accounts::AdminViewPerson uuid={uuid} />
|
||||
),
|
||||
AdminRoute::ViewServiceAccount { uuid } => html!(
|
||||
<components::admin_accounts::AdminViewServiceAccount uuid={uuid} />
|
||||
// html! {<></>}
|
||||
),
|
||||
AdminRoute::ViewOAuth2RP { rs_name } => html! {
|
||||
<components::admin_oauth2::AdminViewOAuth2 rs_name={rs_name} />
|
||||
|
||||
},
|
||||
AdminRoute::NotFound => html! (
|
||||
<Redirect<AdminRoute> to={AdminRoute::NotFound}/>
|
||||
),
|
||||
}
|
||||
}
|
7
server/web_ui/admin/static/wasmloader_admin.js
Normal file
7
server/web_ui/admin/static/wasmloader_admin.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// loads the module which loads the WASM. It's loaders all the way down.
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui_admin.js';
|
||||
async function main() {
|
||||
await init('/pkg/kanidmd_web_ui_admin_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
|
@ -1,44 +1,66 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
# This builds the assets for the Web UI, defaulting to a release build.
|
||||
|
||||
# This builds the assets for the Web UI, defaulting to a release build.
|
||||
if [ ! -f build_wasm.sh ]; then
|
||||
echo "Please run from the crate directory. (server/web_ui)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${BUILD_FLAGS}" ]; then
|
||||
BUILD_FLAGS="--release --no-typescript"
|
||||
fi
|
||||
|
||||
if [ -z "$(which rsync)" ]; then
|
||||
echo "Cannot find rsync which is needed to move things around, quitting!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$(which wasm-pack)" ]; then
|
||||
echo "Cannot find wasm-pack which is needed to build the UI, quitting!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(find ./pkg/ -name 'kanidmd*' | wc -l)" -gt 0 ]; then
|
||||
echo "Cleaning up WASM files before build..."
|
||||
rm pkg/kanidmd*
|
||||
if [ -z "${BUILD_FLAGS}" ]; then
|
||||
export BUILD_FLAGS="--release"
|
||||
fi
|
||||
|
||||
# we can disable this since we want it to expand
|
||||
# shellcheck disable=SC2086
|
||||
wasm-pack build ${BUILD_FLAGS} --target web --mode no-install --no-pack
|
||||
echo "Cleaning up pkg dir"
|
||||
find pkg/ -type f -delete
|
||||
find pkg/ -mindepth 1 -type d -delete
|
||||
|
||||
touch ./pkg/ANYTHING_HERE_WILL_BE_DELETED_ADD_TO_SRC && \
|
||||
rsync --delete-after -r --copy-links -v ./static/* ./pkg/ && \
|
||||
cp ../../README.md ./pkg/
|
||||
cp ../../LICENSE.md ./pkg/
|
||||
touch ./pkg/ANYTHING_HERE_WILL_BE_DELETED_IN_BUILDS
|
||||
# cp ../../README.md ./pkg/
|
||||
# cp ../../LICENSE.md ./pkg/
|
||||
if [ -f ./pkg/.gitignore ]; then
|
||||
rm ./pkg/.gitignore
|
||||
fi
|
||||
|
||||
# copy the shared static things
|
||||
rsync -av shared/static/* shared/static/* pkg/
|
||||
|
||||
|
||||
cd admin
|
||||
echo "building admin"
|
||||
../individual_build.sh || exit 1
|
||||
cd ..
|
||||
echo "done building admin"
|
||||
|
||||
cd login_flows
|
||||
echo "building login_flows"
|
||||
../individual_build.sh || exit 1
|
||||
cd ..
|
||||
echo "done building login_flows"
|
||||
|
||||
cd user
|
||||
echo "building user"
|
||||
../individual_build.sh || exit 1
|
||||
cd ..
|
||||
echo "done building user"
|
||||
|
||||
|
||||
|
||||
if [ -z "${SKIP_BROTLI}" ]; then
|
||||
# updates the brotli-compressed files
|
||||
echo "brotli-compressing the WASM file..."
|
||||
find ./pkg -name '*.wasm' -exec ./find_best_brotli.sh "{}" \; || exit 1
|
||||
fi
|
||||
echo "brotli-compressing compressible files over 16KB in size..."
|
||||
find ./pkg -size +16k -type f \
|
||||
-not -name '*.br' \
|
||||
-not -name '*.png' \
|
||||
-exec ./find_best_brotli.sh "{}" \; || exit 1
|
||||
fi
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
build_wasm.sh
|
|
@ -14,6 +14,7 @@ if [ $# -eq 0 ]; then
|
|||
fi
|
||||
|
||||
filename=$1
|
||||
echo "#####################################"
|
||||
echo "Compressing $1"
|
||||
|
||||
# Exit if the file doesn't exist
|
||||
|
|
45
server/web_ui/individual_build.sh
Executable file
45
server/web_ui/individual_build.sh
Executable file
|
@ -0,0 +1,45 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ ! -f ../individual_build.sh ]; then
|
||||
echo "Please run from the package base directory!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${BUILD_FLAGS}" ]; then
|
||||
BUILD_FLAGS="--release"
|
||||
fi
|
||||
|
||||
if [ -z "$(which rsync)" ]; then
|
||||
echo "Cannot find rsync which is needed to move things around, quitting!"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$(which wasm-pack)" ]; then
|
||||
echo "Cannot find wasm-pack which is needed to build the UI, quitting!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p ./pkg
|
||||
|
||||
echo "Cleaning up WASM files before build..."
|
||||
find ./pkg/ -name 'kanidmd*' -exec rm "{}" \;
|
||||
|
||||
# we can disable this since we want it to expand
|
||||
# shellcheck disable=SC2086
|
||||
wasm-pack build ${BUILD_FLAGS} --no-typescript --target web --mode no-install --no-pack
|
||||
|
||||
echo "######################"
|
||||
echo "Moving files around..."
|
||||
echo "######################"
|
||||
touch ./pkg/ANYTHING_HERE_WILL_BE_DELETED_ADD_TO_SRC && \
|
||||
rm ./pkg/.gitignore
|
||||
|
||||
echo "######################"
|
||||
echo "Moving files up into the webui pkg dir..."
|
||||
echo "######################"
|
||||
rsync -av pkg/* ../pkg/
|
||||
|
||||
echo "######################"
|
||||
echo " Done!"
|
||||
echo "######################"
|
45
server/web_ui/login_flows/Cargo.toml
Normal file
45
server/web_ui/login_flows/Cargo.toml
Normal file
|
@ -0,0 +1,45 @@
|
|||
[package]
|
||||
name = "kanidmd_web_ui_login_flows"
|
||||
description = "Kanidm Server Web UI - Login Flows"
|
||||
documentation = "https://docs.rs/kanidm/latest/kanidm/"
|
||||
|
||||
version = { workspace = true }
|
||||
authors = [
|
||||
"William Brown <william@blackhats.net.au>",
|
||||
"James Hodgkinson <james@terminaloutcomes.com>",
|
||||
]
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
gloo = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
kanidm_proto = { workspace = true, features = ["wasm"] }
|
||||
kanidmd_web_ui_shared = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde-wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
yew = { workspace = true, features = ["csr"] }
|
||||
yew-router = { workspace = true }
|
||||
time = { workspace = true }
|
||||
lazy_static.workspace = true
|
||||
gloo-utils = { workspace = true }
|
||||
web-sys = { workspace = true, features = [
|
||||
"CredentialsContainer",
|
||||
"Location",
|
||||
"Navigator",
|
||||
"Window",
|
||||
] }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = { workspace = true }
|
2
server/web_ui/login_flows/build_dev.sh
Executable file
2
server/web_ui/login_flows/build_dev.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/bin/sh
|
||||
BUILD_FLAGS="--dev" ./build.sh
|
1147
server/web_ui/login_flows/pkg/kanidmd_web_ui_login_flows.js
Normal file
1147
server/web_ui/login_flows/pkg/kanidmd_web_ui_login_flows.js
Normal file
File diff suppressed because it is too large
Load diff
BIN
server/web_ui/login_flows/pkg/kanidmd_web_ui_login_flows_bg.wasm
Normal file
BIN
server/web_ui/login_flows/pkg/kanidmd_web_ui_login_flows_bg.wasm
Normal file
Binary file not shown.
7
server/web_ui/login_flows/pkg/wasmloader_login_flows.js
Normal file
7
server/web_ui/login_flows/pkg/wasmloader_login_flows.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// loads the module which loads the WASM. It's loaders all the way down.
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui_login_flows.js';
|
||||
async function main() {
|
||||
await init('/pkg/kanidmd_web_ui_login_flows_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
|
@ -1,3 +1,5 @@
|
|||
//! Login flow components
|
||||
|
||||
// use anyhow::Error;
|
||||
use gloo::console;
|
||||
use kanidm_proto::v1::{
|
||||
|
@ -5,39 +7,78 @@ use kanidm_proto::v1::{
|
|||
AuthStep,
|
||||
};
|
||||
use kanidm_proto::webauthn::PublicKeyCredential;
|
||||
use kanidmd_web_ui_shared::utils::{autofocus, do_footer, window};
|
||||
use kanidmd_web_ui_shared::{
|
||||
add_body_form_classes, fetch_session_valid, logo_img, remove_body_form_classes, SessionStatus,
|
||||
};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::{spawn_local, JsFuture};
|
||||
use web_sys::CredentialRequestOptions;
|
||||
use yew::prelude::*;
|
||||
use yew::virtual_dom::VNode;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use crate::constants::{CLASS_BUTTON_DARK, CLASS_DIV_LOGIN_BUTTON, CLASS_DIV_LOGIN_FIELD};
|
||||
use crate::error::FetchError;
|
||||
use crate::{do_request, models, utils, RequestMethod};
|
||||
use kanidmd_web_ui_shared::constants::{
|
||||
CLASS_BUTTON_DARK, CLASS_DIV_LOGIN_BUTTON, CLASS_DIV_LOGIN_FIELD, CSS_ALERT_DANGER,
|
||||
URL_USER_HOME,
|
||||
};
|
||||
use kanidmd_web_ui_shared::models::{
|
||||
self, clear_bearer_token, get_bearer_token, get_login_hint, pop_login_hint,
|
||||
pop_login_remember_me, pop_return_location, push_login_remember_me, set_bearer_token,
|
||||
};
|
||||
use kanidmd_web_ui_shared::{do_request, error::FetchError, utils, RequestMethod};
|
||||
use yew_router::BrowserRouter;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LoginApp {
|
||||
state: LoginState,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
impl Default for LoginApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
state: LoginState::InitLogin {
|
||||
enable: true,
|
||||
remember_me: false,
|
||||
username: String::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub enum LoginWorkflow {
|
||||
Login,
|
||||
Reauth,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Properties)]
|
||||
impl std::fmt::Display for LoginWorkflow {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
LoginWorkflow::Login => "LoginWorkflow::Login",
|
||||
LoginWorkflow::Reauth => "LoginWorkflow::Reauth",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LoginWorkflow {
|
||||
fn default() -> Self {
|
||||
Self::Login
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Properties, Default)]
|
||||
pub struct LoginAppProps {
|
||||
pub workflow: LoginWorkflow,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
enum TotpState {
|
||||
Enabled,
|
||||
Disabled,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum LoginState {
|
||||
InitLogin {
|
||||
enable: bool,
|
||||
|
@ -82,6 +123,7 @@ pub enum LoginAppMsg {
|
|||
Select(usize),
|
||||
// DoNothing,
|
||||
UnknownUser,
|
||||
AlreadyAuthenticated,
|
||||
Error { emsg: String, kopid: Option<String> },
|
||||
}
|
||||
|
||||
|
@ -94,7 +136,22 @@ impl From<FetchError> for LoginAppMsg {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<SessionStatus> for LoginAppMsg {
|
||||
fn from(s: SessionStatus) -> Self {
|
||||
match s {
|
||||
SessionStatus::TokenValid => LoginAppMsg::AlreadyAuthenticated,
|
||||
SessionStatus::LoginRequired => LoginAppMsg::Begin,
|
||||
SessionStatus::Error { emsg, kopid } => LoginAppMsg::Error { emsg, kopid },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LoginApp {
|
||||
/// Validate that the current auth token's OK
|
||||
async fn fetch_session_valid() -> Result<LoginAppMsg, FetchError> {
|
||||
fetch_session_valid().await.map(|v| v.into())
|
||||
}
|
||||
|
||||
async fn auth_init(username: String) -> Result<LoginAppMsg, FetchError> {
|
||||
let authreq = AuthRequest {
|
||||
step: AuthStep::Init2 {
|
||||
|
@ -138,11 +195,11 @@ impl LoginApp {
|
|||
|
||||
if status == 200 {
|
||||
let state: AuthResponse = serde_wasm_bindgen::from_value(value)
|
||||
.expect_throw("Invalid response type - auth_init::AuthResponse");
|
||||
.expect_throw("Invalid response type during reauth_init::AuthResponse");
|
||||
Ok(LoginAppMsg::Next(state))
|
||||
} else if status == 404 {
|
||||
console::error!(format!(
|
||||
"User not found: {:?}. Operation ID: {:?}",
|
||||
"User not found during reauth_init: {:?}. Operation ID: {:?}",
|
||||
value.as_string(),
|
||||
kopid
|
||||
));
|
||||
|
@ -180,8 +237,9 @@ impl LoginApp {
|
|||
fn button_start_again(&self, ctx: &Context<Self>) -> VNode {
|
||||
html! {
|
||||
<div class="col-md-auto text-center">
|
||||
// TODO: this doesn't seem to work if you failed to login
|
||||
<button type="button" class={CLASS_BUTTON_DARK} onclick={ ctx.link().callback(|_| LoginAppMsg::Restart) } >{" Start Again "}</button>
|
||||
<button type="button" class={CLASS_BUTTON_DARK} onclick={ ctx.link().callback(|_| LoginAppMsg::Restart) } >
|
||||
{" Start Again "}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +278,7 @@ impl LoginApp {
|
|||
html! {
|
||||
<div class="container">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<div class={CSS_ALERT_DANGER} role="alert">
|
||||
<p><strong>{ alert_title }</strong></p>
|
||||
if let Some(value) = alert_message {
|
||||
<p>{ value }</p>
|
||||
|
@ -263,6 +321,7 @@ impl LoginApp {
|
|||
type="text"
|
||||
autocomplete="username"
|
||||
value={ username }
|
||||
required=true
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -542,18 +601,21 @@ impl LoginApp {
|
|||
}
|
||||
}
|
||||
LoginState::Authenticated => {
|
||||
let loc = models::pop_return_location();
|
||||
// redirect
|
||||
let loc = pop_return_location();
|
||||
// redirect to the "return location"
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(format!("authenticated, try going to -> {:?}", loc));
|
||||
loc.goto(
|
||||
&ctx.link()
|
||||
.navigator()
|
||||
.expect_throw("failed to read history"),
|
||||
);
|
||||
console::debug!(format!("authenticated, trying to go to {:?}", loc));
|
||||
|
||||
let window = gloo_utils::window();
|
||||
window
|
||||
.location()
|
||||
.set_href(&loc)
|
||||
.expect_throw(&format!("failed to set location to {}", loc));
|
||||
// this isn't likely to actually render but we might as well...
|
||||
html! {
|
||||
<div class="alert alert-success">
|
||||
<h3>{ "Login Success 🎉" }</h3>
|
||||
<a href={loc}>{"Click here to continue if you aren't redirected..."}</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
@ -588,17 +650,48 @@ impl Component for LoginApp {
|
|||
type Properties = LoginAppProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("login::create".to_string());
|
||||
let workflow = ctx.props().workflow.to_owned();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(&format!("login::create -> workflow: {}", workflow));
|
||||
|
||||
let workflow = &ctx.props().workflow;
|
||||
let state = match workflow {
|
||||
LoginWorkflow::Login => {
|
||||
// Assume we are here for a good reason.
|
||||
// -- clear the bearer to prevent conflict
|
||||
models::clear_bearer_token();
|
||||
// let's check if they're already authenticated!
|
||||
if get_bearer_token().is_some() {
|
||||
ctx.link().send_future(async {
|
||||
match Self::fetch_session_valid().await {
|
||||
Ok(_) => {
|
||||
console::info!(
|
||||
"Already logged in, redirecting to user home page"
|
||||
);
|
||||
let window = gloo_utils::window();
|
||||
window
|
||||
.location()
|
||||
.set_href(URL_USER_HOME)
|
||||
.expect_throw(&["failed to set location to ", URL_USER_HOME].concat());
|
||||
|
||||
LoginAppMsg::AlreadyAuthenticated
|
||||
}
|
||||
Err(v) => {
|
||||
console::error!(
|
||||
"Error checking session validity, clearing token and returning to login page: {:?}",
|
||||
v.as_string()
|
||||
);
|
||||
clear_bearer_token();
|
||||
LoginAppMsg::Restart
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if get_bearer_token().is_some() {
|
||||
// We're already logged in, so we're going to redirect to the apps page.
|
||||
return Self::default();
|
||||
}
|
||||
|
||||
// Do we have a login hint?
|
||||
let (username, remember_me) = models::get_login_hint()
|
||||
let (username, remember_me) = get_login_hint()
|
||||
.map(|user| (user, false))
|
||||
.or_else(|| models::get_login_remember_me().map(|user| (user, true)))
|
||||
.unwrap_or_default();
|
||||
|
@ -609,18 +702,13 @@ impl Component for LoginApp {
|
|||
username,
|
||||
}
|
||||
}
|
||||
LoginWorkflow::Reauth => {
|
||||
// Unlike login, don't clear tokens or cookies - these are needed during the operation
|
||||
// to actually start the reauth as the same user.
|
||||
|
||||
match models::get_login_hint() {
|
||||
Some(spn) => LoginState::InitReauth { enable: true, spn },
|
||||
None => LoginState::Error {
|
||||
emsg: "Client Error - No login hint available".to_string(),
|
||||
kopid: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
LoginWorkflow::Reauth => match get_login_hint() {
|
||||
Some(spn) => LoginState::InitReauth { enable: true, spn },
|
||||
None => LoginState::Error {
|
||||
emsg: "Client Error - No login hint available".to_string(),
|
||||
kopid: None,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
add_body_form_classes!();
|
||||
|
@ -634,11 +722,18 @@ impl Component for LoginApp {
|
|||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
LoginAppMsg::AlreadyAuthenticated => {
|
||||
#[cfg(debug_assertions)]
|
||||
console::info!("User is already authenticated, redirecting to user home");
|
||||
window().location().set_href(URL_USER_HOME).unwrap_throw();
|
||||
// no need to render it, we're going away now
|
||||
false
|
||||
}
|
||||
LoginAppMsg::Restart => {
|
||||
// Clear any leftover input. Reset to the remembered username if any.
|
||||
match &ctx.props().workflow {
|
||||
LoginWorkflow::Login => {
|
||||
let (username, remember_me) = models::get_login_hint()
|
||||
let (username, remember_me) = get_login_hint()
|
||||
.map(|user| (user, false))
|
||||
.or_else(|| models::get_login_remember_me().map(|user| (user, true)))
|
||||
.unwrap_or_default();
|
||||
|
@ -650,8 +745,14 @@ impl Component for LoginApp {
|
|||
};
|
||||
}
|
||||
LoginWorkflow::Reauth => {
|
||||
match models::get_login_hint() {
|
||||
Some(spn) => LoginState::InitReauth { enable: true, spn },
|
||||
match get_login_hint() {
|
||||
Some(spn) => {
|
||||
self.state = LoginState::InitReauth {
|
||||
enable: true,
|
||||
spn: spn.clone(),
|
||||
};
|
||||
LoginState::InitReauth { enable: true, spn }
|
||||
}
|
||||
None => LoginState::Error {
|
||||
emsg: "Client Error - No login hint available".to_string(),
|
||||
kopid: None,
|
||||
|
@ -678,10 +779,10 @@ impl Component for LoginApp {
|
|||
.map(|element| element.checked())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
models::push_login_remember_me(username.clone());
|
||||
push_login_remember_me(username.clone());
|
||||
true
|
||||
} else {
|
||||
models::pop_login_remember_me();
|
||||
pop_login_remember_me();
|
||||
false
|
||||
};
|
||||
|
||||
|
@ -711,7 +812,7 @@ impl Component for LoginApp {
|
|||
}
|
||||
});
|
||||
|
||||
self.state = match models::get_login_hint() {
|
||||
self.state = match get_login_hint() {
|
||||
Some(spn) => LoginState::InitReauth { enable: false, spn },
|
||||
None => LoginState::Error {
|
||||
emsg: "Client Error - No login hint available".to_string(),
|
||||
|
@ -934,21 +1035,25 @@ impl Component for LoginApp {
|
|||
} else {
|
||||
// Else, present the options in a choice.
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("multiple choices exist".to_string());
|
||||
console::debug!("multiple auth method choices exist!");
|
||||
self.state = LoginState::Continue(allowed);
|
||||
}
|
||||
true
|
||||
}
|
||||
AuthState::Denied(reason) => {
|
||||
console::error!(format!("denied -> {:?}", reason));
|
||||
console::error!(format!("Authentication denied -> {:?}", reason));
|
||||
self.state = LoginState::Denied(reason);
|
||||
true
|
||||
}
|
||||
AuthState::Success(bearer_token) => {
|
||||
// Store the bearer here!
|
||||
// We need to format the bearer onto it.
|
||||
#[cfg(debug_assertions)]
|
||||
console::info!(
|
||||
"User has successfully authenticated, setting the bearer token"
|
||||
);
|
||||
let bearer_token = format!("Bearer {}", bearer_token);
|
||||
models::set_bearer_token(bearer_token);
|
||||
set_bearer_token(bearer_token);
|
||||
self.state = LoginState::Authenticated;
|
||||
true
|
||||
}
|
||||
|
@ -1017,40 +1122,20 @@ impl Component for LoginApp {
|
|||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("login::view".to_string());
|
||||
// How do we add a top level theme?
|
||||
/*
|
||||
let (width, height): (u32, u32) = if let Some(win) = web_sys::window() {
|
||||
let w = win.inner_width().unwrap();
|
||||
let h = win.inner_height().unwrap();
|
||||
ConsoleService::log(format!("width {:?} {:?}", w, w.as_f64()).as_str());
|
||||
ConsoleService::log(format!("height {:?} {:?}", h, h.as_f64()).as_str());
|
||||
(w.as_f64().unwrap() as u32, h.as_f64().unwrap() as u32)
|
||||
} else {
|
||||
ConsoleService::log("Unable to access document window");
|
||||
(0, 0)
|
||||
};
|
||||
let (width, height) = (width.to_string(), height.to_string());
|
||||
*/
|
||||
|
||||
// <canvas id="confetti-canvas" style="position:absolute" width=width height=height></canvas>
|
||||
|
||||
// May need to set these classes?
|
||||
// <body class="html-body form-body">
|
||||
// TODO: add the domain_display_name here
|
||||
|
||||
html! {
|
||||
<>
|
||||
<main class="flex-shrink-0 form-signin">
|
||||
<center>
|
||||
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
|
||||
// TODO: replace this with a call to domain info
|
||||
// More likely we should have this passed in from the props when we start.
|
||||
<h3>{ "Kanidm" }</h3>
|
||||
</center>
|
||||
{ self.view_state(ctx) }
|
||||
</main>
|
||||
{ crate::utils::do_footer() }
|
||||
</>
|
||||
<BrowserRouter>
|
||||
<main class="flex-shrink-0 form-signin">
|
||||
<center>
|
||||
{logo_img()}
|
||||
// TODO: make a call to domain info to show the domain name
|
||||
// or more likely we should have this passed in from the props when we start.
|
||||
<h3>{ "Kanidm" }</h3>
|
||||
</center>
|
||||
// <Switch<LoginRoute> render={switch} />
|
||||
{ self.view_state(ctx) }
|
||||
</main>
|
||||
{ do_footer() }
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1059,7 +1144,7 @@ impl Component for LoginApp {
|
|||
console::debug!("login::destroy".to_string());
|
||||
|
||||
// Done with this, clear it.
|
||||
let _ = models::pop_login_hint();
|
||||
let _ = pop_login_hint();
|
||||
|
||||
remove_body_form_classes!();
|
||||
}
|
||||
|
@ -1068,10 +1153,10 @@ impl Component for LoginApp {
|
|||
#[cfg(debug_assertions)]
|
||||
console::debug!("login::rendered".to_string());
|
||||
// Force autofocus on elements that need it if present.
|
||||
crate::utils::autofocus("username");
|
||||
crate::utils::autofocus("password");
|
||||
crate::utils::autofocus("backup_code");
|
||||
crate::utils::autofocus("otp");
|
||||
crate::utils::autofocus("begin");
|
||||
autofocus("username");
|
||||
autofocus("password");
|
||||
autofocus("backup_code");
|
||||
autofocus("otp");
|
||||
autofocus("begin");
|
||||
}
|
||||
}
|
73
server/web_ui/login_flows/src/lib.rs
Normal file
73
server/web_ui/login_flows/src/lib.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
//! This handles the login/auth flows, and is designed to be smol and snappy
|
||||
//! so it loads fast and gets the user to where they need to go!
|
||||
//!
|
||||
//! - /ui/login
|
||||
//! - /ui/oauth2
|
||||
//! - /ui/reauth
|
||||
|
||||
mod components;
|
||||
mod oauth2;
|
||||
pub mod router;
|
||||
|
||||
use gloo::console;
|
||||
use kanidmd_web_ui_shared::constants::URL_LOGIN;
|
||||
use kanidmd_web_ui_shared::utils::window;
|
||||
use router::LoginRoute;
|
||||
#[allow(unused_imports)] // because it's needed to compile wasm things
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
|
||||
use wasm_bindgen::{JsValue, UnwrapThrowExt};
|
||||
use yew::{html, Html};
|
||||
use yew_router::{BrowserRouter, Switch};
|
||||
|
||||
use crate::components::{LoginApp, LoginWorkflow};
|
||||
use crate::oauth2::Oauth2App;
|
||||
|
||||
// Needed for yew to pass by value
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
/// Handle routes for the login_flows app
|
||||
fn switch(route: LoginRoute) -> Html {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(format!("UserUiApp::switch -> {:?}", route).as_str());
|
||||
match route {
|
||||
LoginRoute::Login => html! {<LoginApp workflow={LoginWorkflow::Login} />},
|
||||
LoginRoute::Reauth => html! {<LoginApp workflow={LoginWorkflow::Reauth} />},
|
||||
LoginRoute::Oauth2 => html! {<Oauth2App />},
|
||||
LoginRoute::NotFound => {
|
||||
console::error!("Unknown route {}, showing login flow");
|
||||
window()
|
||||
.location()
|
||||
.set_href(URL_LOGIN)
|
||||
.expect_throw("Failed to redirect user to the login page!");
|
||||
html! { <a href={URL_LOGIN}>{"Click here to return to the login page..."}</a> }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoginFlowsApp {}
|
||||
|
||||
impl yew::Component for LoginFlowsApp {
|
||||
type Message = ();
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &yew::Context<Self>) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &yew::Context<Self>) -> Html {
|
||||
html! {
|
||||
<BrowserRouter>
|
||||
<Switch<LoginRoute> render={switch} />
|
||||
</BrowserRouter>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This is the entry point of the web front end.
|
||||
///
|
||||
/// This triggers the manager app to load and begin its event loop.
|
||||
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
|
||||
pub fn run_app() -> Result<(), JsValue> {
|
||||
yew::Renderer::<LoginFlowsApp>::new().render();
|
||||
Ok(())
|
||||
}
|
|
@ -1,24 +1,33 @@
|
|||
use gloo::console;
|
||||
use kanidm_proto::constants::APPLICATION_JSON;
|
||||
use kanidm_proto::constants::uri::{OAUTH2_AUTHORISE, OAUTH2_AUTHORISE_PERMIT};
|
||||
use kanidm_proto::constants::{APPLICATION_JSON, KOPID};
|
||||
pub use kanidm_proto::oauth2::{
|
||||
AccessTokenRequest, AccessTokenResponse, AuthorisationRequest, AuthorisationResponse,
|
||||
CodeChallengeMethod, ErrorResponse,
|
||||
};
|
||||
use kanidmd_web_ui_shared::constants::{CONTENT_TYPE, CSS_ALERT_DANGER, URL_OAUTH2};
|
||||
use kanidmd_web_ui_shared::utils::{do_alert_error, do_footer, window};
|
||||
use kanidmd_web_ui_shared::{
|
||||
add_body_form_classes, fetch_session_valid, logo_img, remove_body_form_classes, SessionStatus,
|
||||
};
|
||||
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, RequestRedirect, Response};
|
||||
use yew::prelude::*;
|
||||
use yew_router::prelude::*;
|
||||
|
||||
use crate::manager::Route;
|
||||
use crate::{do_request, error::*, RequestMethod};
|
||||
use crate::{models, utils};
|
||||
use super::router::LoginRoute;
|
||||
use kanidmd_web_ui_shared::models::{
|
||||
get_bearer_token, pop_oauth2_authorisation_request, push_login_hint,
|
||||
push_oauth2_authorisation_request, push_return_location,
|
||||
};
|
||||
use kanidmd_web_ui_shared::{do_request, error::FetchError, utils, RequestMethod};
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
enum State {
|
||||
LoginRequired,
|
||||
// We are in the process of check the auth token to be sure we can proceed.
|
||||
// We are in the process of checking the auth token to be sure we can proceed.
|
||||
TokenCheck,
|
||||
// Token check done, lets do it.
|
||||
SubmitAuthReq,
|
||||
|
@ -69,36 +78,32 @@ impl From<FetchError> for Oauth2Msg {
|
|||
}
|
||||
}
|
||||
|
||||
impl Oauth2App {
|
||||
async fn fetch_session_valid() -> Result<Oauth2Msg, FetchError> {
|
||||
let (kopid, status, value, _) =
|
||||
do_request("/v1/auth/valid", RequestMethod::GET, None).await?;
|
||||
|
||||
if status == 200 {
|
||||
Ok(Oauth2Msg::TokenValid)
|
||||
} else if status == 401 {
|
||||
Ok(Oauth2Msg::LoginRequired)
|
||||
} else {
|
||||
let emsg = value.as_string().unwrap_or_default();
|
||||
// let jsval_json = JsFuture::from(resp.json()?).await?;
|
||||
Ok(Oauth2Msg::Error { emsg, kopid })
|
||||
impl From<SessionStatus> for Oauth2Msg {
|
||||
fn from(value: SessionStatus) -> Self {
|
||||
match value {
|
||||
SessionStatus::TokenValid => Oauth2Msg::TokenValid,
|
||||
SessionStatus::LoginRequired => Oauth2Msg::LoginRequired,
|
||||
SessionStatus::Error { emsg, kopid } => Oauth2Msg::Error { emsg, kopid },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Oauth2App {
|
||||
/// Validate that the current auth token's OK
|
||||
async fn fetch_session_valid() -> Result<Oauth2Msg, FetchError> {
|
||||
fetch_session_valid().await.map(|v| v.into())
|
||||
}
|
||||
|
||||
async fn fetch_authreq(authreq: AuthorisationRequest) -> Result<Oauth2Msg, FetchError> {
|
||||
let authreq_jsvalue = serde_json::to_string(&authreq)
|
||||
.map(|s| JsValue::from(&s))
|
||||
.expect_throw("Failed to serialise authreq");
|
||||
|
||||
let (kopid, status, value, headers) = do_request(
|
||||
"/oauth2/authorise",
|
||||
RequestMethod::POST,
|
||||
Some(authreq_jsvalue),
|
||||
)
|
||||
.await?;
|
||||
let (kopid, status, value, headers) =
|
||||
do_request(OAUTH2_AUTHORISE, RequestMethod::POST, Some(authreq_jsvalue)).await?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(&format!("fetch_authreq {}", status));
|
||||
console::debug!(&format!("fetch_authreq result {}", status));
|
||||
|
||||
if status == 200 {
|
||||
let state: AuthorisationResponse = serde_wasm_bindgen::from_value(value)
|
||||
|
@ -150,14 +155,14 @@ impl Oauth2App {
|
|||
|
||||
opts.body(Some(&consentreq_jsvalue));
|
||||
|
||||
let request = Request::new_with_str_and_init("/oauth2/authorise/permit", &opts)?;
|
||||
let request = Request::new_with_str_and_init(OAUTH2_AUTHORISE_PERMIT, &opts)?;
|
||||
|
||||
request
|
||||
.headers()
|
||||
.set(crate::constants::CONTENT_TYPE, APPLICATION_JSON)
|
||||
.set(CONTENT_TYPE, APPLICATION_JSON)
|
||||
.expect_throw("failed to set header");
|
||||
|
||||
if let Some(bearer_token) = models::get_bearer_token() {
|
||||
if let Some(bearer_token) = get_bearer_token() {
|
||||
request
|
||||
.headers()
|
||||
.set("authorization", &bearer_token)
|
||||
|
@ -170,7 +175,7 @@ impl Oauth2App {
|
|||
let status = resp.status();
|
||||
let headers = resp.headers();
|
||||
|
||||
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||
let kopid = headers.get(KOPID).ok().flatten();
|
||||
|
||||
if status == 200 {
|
||||
if let Some(loc) = headers.get("location").ok().flatten() {
|
||||
|
@ -215,7 +220,7 @@ impl Component for Oauth2App {
|
|||
.ok()
|
||||
.or_else(|| {
|
||||
console::log!("using previously storage oauth2 authorisation request if possible");
|
||||
models::pop_oauth2_authorisation_request()
|
||||
pop_oauth2_authorisation_request()
|
||||
});
|
||||
|
||||
add_body_form_classes!();
|
||||
|
@ -236,11 +241,11 @@ impl Component for Oauth2App {
|
|||
// See: https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
|
||||
// specifically, login_hint
|
||||
if let Some(login_hint) = query.oidc_ext.login_hint.clone() {
|
||||
models::push_login_hint(login_hint)
|
||||
push_login_hint(login_hint)
|
||||
}
|
||||
// Push the request down. This covers if we move to LoginRequired so we can restore where
|
||||
// we were / what we were doing.
|
||||
models::push_oauth2_authorisation_request(query);
|
||||
push_oauth2_authorisation_request(query);
|
||||
|
||||
// Start the fetch req.
|
||||
// Put the fetch handle into the consent type.
|
||||
|
@ -272,18 +277,22 @@ impl Component for Oauth2App {
|
|||
true
|
||||
}
|
||||
Oauth2Msg::LoginProceed => {
|
||||
models::push_return_location(models::Location::Manager(Route::Oauth2));
|
||||
let current_loc = window()
|
||||
.location()
|
||||
.as_string()
|
||||
.unwrap_or(URL_OAUTH2.to_string());
|
||||
push_return_location(¤t_loc);
|
||||
|
||||
ctx.link()
|
||||
.navigator()
|
||||
.expect_throw("failed to read history")
|
||||
.push(&Route::Login);
|
||||
.push(&LoginRoute::Login);
|
||||
// Don't need to redraw as we are yolo-ing out.
|
||||
false
|
||||
}
|
||||
Oauth2Msg::TokenValid => {
|
||||
// Okay we can proceed, pop the query.
|
||||
let ar = models::pop_oauth2_authorisation_request();
|
||||
let ar = pop_oauth2_authorisation_request();
|
||||
|
||||
self.state = match (&self.state, ar) {
|
||||
(State::TokenCheck, Some(ar)) => {
|
||||
|
@ -402,7 +411,7 @@ impl Component for Oauth2App {
|
|||
action="javascript:void(0);"
|
||||
>
|
||||
<h1 class="h3 mb-3 fw-normal">
|
||||
// TODO: include the domain display name here
|
||||
// TODO: include the domain display name here, and the RS display name?
|
||||
{"Sign in to proceed" }
|
||||
</h1>
|
||||
<button autofocus=true class="w-100 btn btn-lg btn-primary" type="submit">
|
||||
|
@ -476,7 +485,7 @@ impl Component for Oauth2App {
|
|||
}
|
||||
State::AccessDenied(kopid) => {
|
||||
html! {
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<div class={CSS_ALERT_DANGER} role="alert">
|
||||
<h1>{ "Access Denied" } </h1>
|
||||
<p>
|
||||
{ "You do not have access to the requested resources." }
|
||||
|
@ -492,28 +501,23 @@ impl Component for Oauth2App {
|
|||
</div>
|
||||
}
|
||||
}
|
||||
State::ErrInvalidRequest => {
|
||||
html! {
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<h1>{ "Invalid request" } </h1>
|
||||
<p>
|
||||
{ "Please close this window and try again again from the beginning." }
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
State::ErrInvalidRequest => do_alert_error(
|
||||
"Invalid request",
|
||||
Some("Please close this window and try again again from the beginning."),
|
||||
false,
|
||||
),
|
||||
};
|
||||
html! {
|
||||
<>
|
||||
<main class="form-signin">
|
||||
<center>
|
||||
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
|
||||
{logo_img()}
|
||||
</center>
|
||||
<div class="container">
|
||||
{ body_content }
|
||||
</div>
|
||||
</main>
|
||||
{ crate::utils::do_footer() }
|
||||
{ do_footer() }
|
||||
</>
|
||||
}
|
||||
}
|
18
server/web_ui/login_flows/src/router.rs
Normal file
18
server/web_ui/login_flows/src/router.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
#![allow(clippy::disallowed_types)] // because `Routable` uses a hashmap
|
||||
use serde::{Deserialize, Serialize};
|
||||
use yew_router::Routable;
|
||||
|
||||
#[derive(Routable, PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum LoginRoute {
|
||||
#[at("/ui/login")]
|
||||
Login,
|
||||
#[at("/ui/reauth")]
|
||||
Reauth,
|
||||
|
||||
#[at("/ui/oauth2")]
|
||||
Oauth2,
|
||||
|
||||
#[not_found]
|
||||
#[at("/ui/login/404")]
|
||||
NotFound,
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// loads the module which loads the WASM. It's loaders all the way down.
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui_login_flows.js';
|
||||
async function main() {
|
||||
await init('/pkg/kanidmd_web_ui_login_flows_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
|
@ -1,144 +0,0 @@
|
|||
# Kanidm - Simple and Secure Identity Management
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/kanidm/kanidm/master/artwork/logo-small.png" width="20%" height="auto" />
|
||||
</p>
|
||||
|
||||
## About
|
||||
|
||||
Kanidm is a simple and secure identity management platform, allowing other applications and services
|
||||
to offload the challenge of authenticating and storing identities to Kanidm.
|
||||
|
||||
The goal of this project is to be a complete identity provider, covering the broadest possible set
|
||||
of requirements and integrations. You should not need any other components (like Keycloak) when you
|
||||
use Kanidm - we already have everything you need!
|
||||
|
||||
To achieve this we rely heavily on strict defaults, simple configuration, and self-healing
|
||||
components. This allows Kanidm to run from small home labs, for families, small business, and all
|
||||
the way to the largest enterprise needs.
|
||||
|
||||
If you want to host your own authentication service, then Kanidm is for you!
|
||||
|
||||
<details><summary>Supported Features</summary>
|
||||
|
||||
Kanidm supports:
|
||||
|
||||
- Webauthn (passkeys) for secure cryptographic authentication
|
||||
- Oauth2/OIDC Authentication provider for web SSO
|
||||
- Oauth Application Portal/Gateway allowing easy access to linked applications
|
||||
- Linux/Unix integration with offline authentication
|
||||
- SSH key distribution to Linux/Unix systems
|
||||
- RADIUS for network and VPN authentication
|
||||
- Read only LDAPS gateway for Legacy Systems
|
||||
- Complete CLI tooling for Administration
|
||||
- User Self Service the WebUI
|
||||
|
||||
</details>
|
||||
|
||||
## Documentation / Getting Started / Install
|
||||
|
||||
If you want to read more about what Kanidm can do, you should read our documentation.
|
||||
|
||||
- [Kanidm book (Latest stable)](https://kanidm.github.io/kanidm/stable/)
|
||||
|
||||
We also have a set of
|
||||
[support guidelines](https://github.com/kanidm/kanidm/blob/master/project_docs/RELEASE_AND_SUPPORT.md)
|
||||
for what the project team will support
|
||||
|
||||
## Code of Conduct / Ethics
|
||||
|
||||
All interactions with the project are covered by our [code of conduct].
|
||||
|
||||
When we develop features we follow our projects guidelines on [rights and ethics]
|
||||
|
||||
[code of conduct]: https://github.com/kanidm/kanidm/blob/master/CODE_OF_CONDUCT.md
|
||||
[rights and ethics]: https://github.com/kanidm/kanidm/blob/master/project_docs/ethics/README.md
|
||||
|
||||
## Getting in Contact / Questions
|
||||
|
||||
We have a [gitter community channel] where project members are always happy to answer questions.
|
||||
Alternately you can open a new [github discussion].
|
||||
|
||||
[gitter community channel]: https://gitter.im/kanidm/community
|
||||
[github discussion]: https://github.com/kanidm/kanidm/discussions
|
||||
|
||||
## What does Kanidm mean?
|
||||
|
||||
Kanidm is a portmanteau of 'kani' and 'idm'. Kani is Japanese for crab, related to Rust's mascot
|
||||
ferris the crab. identity management is often abbreviated to 'idm', and is a common industry term
|
||||
for these services.
|
||||
|
||||
Kanidm is pronounced as "kar - nee - dee - em".
|
||||
|
||||
## Comparison with other services
|
||||
|
||||
<details><summary>LLDAP</summary>
|
||||
|
||||
[LLDAP](https://github.com/nitnelave/lldap) is a similar project aiming for a small and easy to
|
||||
administer LDAP server with a web administration portal. Both projects use the
|
||||
[Kanidm LDAP bindings](https://github.com/kanidm/ldap3), and have many similar ideas.
|
||||
|
||||
The primary benefit of Kanidm over LLDAP is that Kanidm offers a broader set of "built in" features
|
||||
like Oauth2 and OIDC. To use these from LLDAP you need an external portal like Keycloak, where in
|
||||
Kanidm they are "built in". However that is also a strength of LLDAP is that is offers "less" which
|
||||
may make it easier to administer and deploy for you.
|
||||
|
||||
If Kanidm is too complex for your needs, you should check out LLDAP as a smaller alternative. If you
|
||||
want a project which has a broader feature set out of the box, then Kanidm might be a better fit.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>389-ds / OpenLDAP</summary>
|
||||
Both 389-ds and OpenLDAP are generic LDAP servers. This means they only provide LDAP and you need to
|
||||
bring your own IDM components - you need your own OIDC portal, webui's for self service, commandline
|
||||
tools to administer and more.
|
||||
|
||||
If you need the highest levels of customisation possible from your LDAP deployment, then these are
|
||||
probably better alternatives. If you want a service that is easy to setup and focused on IDM, then
|
||||
Kanidm is a better choice.
|
||||
|
||||
Kanidm was originally inspired by many elements of both 389-ds and OpenLDAP. Already Kanidm is as
|
||||
fast as (or faster than) 389-ds for performance and scaling as a directory service.
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>FreeIPA</summary>
|
||||
FreeIPA is another identity management service for Linux/Unix, and ships a huge number of features
|
||||
from LDAP, Kerberos, DNS, Certificate Authority, and more.
|
||||
|
||||
FreeIPA however is a complex system, with a huge amount of parts and configuration. This adds a lot
|
||||
of resource overhead and difficulty for administration.
|
||||
|
||||
Kanidm aims to have the features richness of FreeIPA, but without the resource and administration
|
||||
overheads. If you want a complete IDM package, but in a lighter footprint and easier to manage, then
|
||||
Kanidm is probably for you. In testing with 3000 users + 1500 groups, Kanidm is 3 times faster for
|
||||
search operations and 5 times faster for modification and addition of entries (your results may
|
||||
differ however, but generally Kanidm is much faster than FreeIPA).
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>Keycloak</summary>
|
||||
Keycloak is an OIDC/Oauth2/SAML provider. It allows you to layer on Webauthn to existing IDM systems.
|
||||
Keycloak can operate as a stand alone IDM but generally is a component attached to an existing LDAP
|
||||
server or similar.
|
||||
|
||||
Keycloak requires a significant amount of configuration and experience to deploy. It allows high
|
||||
levels of customisation to every detail of it's authentication work flows, which makes it harder to
|
||||
start with in many cases.
|
||||
|
||||
Kanidm does NOT require Keycloak to provide services such as Oauth2 and integrates many of the
|
||||
elements in a simpler and correct way out of the box in comparison.
|
||||
|
||||
</details>
|
||||
|
||||
## Developer Getting Started
|
||||
|
||||
If you want to contribute to Kanidm there is a getting started [guide for developers]. IDM is a
|
||||
diverse topic and we encourage contributions of many kinds in the project, from people of all
|
||||
backgrounds.
|
||||
|
||||
When developing the server you should refer to the latest commit documentation instead.
|
||||
|
||||
- [Kanidm book (Latest commit)](https://kanidm.github.io/kanidm/master/)
|
||||
|
||||
[guide for developers]: https://kanidm.github.io/kanidm/master/DEVELOPER_README.html
|
BIN
server/web_ui/pkg/external/bootstrap.bundle.min.js.br
vendored
Normal file
BIN
server/web_ui/pkg/external/bootstrap.bundle.min.js.br
vendored
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/external/bootstrap.bundle.min.js.map.br
vendored
Normal file
BIN
server/web_ui/pkg/external/bootstrap.bundle.min.js.map.br
vendored
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/external/bootstrap.min.css.br
vendored
Normal file
BIN
server/web_ui/pkg/external/bootstrap.min.css.br
vendored
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/external/bootstrap.min.css.map.br
vendored
Normal file
BIN
server/web_ui/pkg/external/bootstrap.min.css.map.br
vendored
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/img/icon-accounts.svg.br
Normal file
BIN
server/web_ui/pkg/img/icon-accounts.svg.br
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/img/icon-groups.svg.br
Normal file
BIN
server/web_ui/pkg/img/icon-groups.svg.br
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/img/icon-oauth2.svg.br
Normal file
BIN
server/web_ui/pkg/img/icon-oauth2.svg.br
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/img/icon-person.svg.br
Normal file
BIN
server/web_ui/pkg/img/icon-person.svg.br
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/img/icon-robot.svg.br
Normal file
BIN
server/web_ui/pkg/img/icon-robot.svg.br
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/img/logo-square.svg.br
Normal file
BIN
server/web_ui/pkg/img/logo-square.svg.br
Normal file
Binary file not shown.
1033
server/web_ui/pkg/kanidmd_web_ui_admin.js
Normal file
1033
server/web_ui/pkg/kanidmd_web_ui_admin.js
Normal file
File diff suppressed because it is too large
Load diff
BIN
server/web_ui/pkg/kanidmd_web_ui_admin.js.br
Normal file
BIN
server/web_ui/pkg/kanidmd_web_ui_admin.js.br
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/kanidmd_web_ui_admin_bg.wasm
Normal file
BIN
server/web_ui/pkg/kanidmd_web_ui_admin_bg.wasm
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/kanidmd_web_ui_admin_bg.wasm.br
Normal file
BIN
server/web_ui/pkg/kanidmd_web_ui_admin_bg.wasm.br
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
1147
server/web_ui/pkg/kanidmd_web_ui_login_flows.js
Normal file
1147
server/web_ui/pkg/kanidmd_web_ui_login_flows.js
Normal file
File diff suppressed because it is too large
Load diff
BIN
server/web_ui/pkg/kanidmd_web_ui_login_flows.js.br
Normal file
BIN
server/web_ui/pkg/kanidmd_web_ui_login_flows.js.br
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/kanidmd_web_ui_login_flows_bg.wasm
Normal file
BIN
server/web_ui/pkg/kanidmd_web_ui_login_flows_bg.wasm
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/kanidmd_web_ui_login_flows_bg.wasm.br
Normal file
BIN
server/web_ui/pkg/kanidmd_web_ui_login_flows_bg.wasm.br
Normal file
Binary file not shown.
|
@ -1,4 +1,4 @@
|
|||
import { modal_hide_by_id } from '/pkg/wasmloader.js';
|
||||
import { modal_hide_by_id } from '/pkg/shared.js';
|
||||
|
||||
let wasm;
|
||||
|
||||
|
@ -225,7 +225,7 @@ function makeMutClosure(arg0, arg1, dtor, f) {
|
|||
return real;
|
||||
}
|
||||
function __wbg_adapter_48(arg0, arg1) {
|
||||
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hee64a599f575a1b3(arg0, arg1);
|
||||
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h29b65e42a9dd1ad6(arg0, arg1);
|
||||
}
|
||||
|
||||
let stack_pointer = 128;
|
||||
|
@ -237,19 +237,19 @@ function addBorrowedObject(obj) {
|
|||
}
|
||||
function __wbg_adapter_51(arg0, arg1, arg2) {
|
||||
try {
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6821a64a8b32b54e(arg0, arg1, addBorrowedObject(arg2));
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h17983a2b133dd53c(arg0, arg1, addBorrowedObject(arg2));
|
||||
} finally {
|
||||
heap[stack_pointer++] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function __wbg_adapter_54(arg0, arg1, arg2) {
|
||||
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h578c1a08304759e7(arg0, arg1, addHeapObject(arg2));
|
||||
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h5882a5a28fa681a9(arg0, arg1, addHeapObject(arg2));
|
||||
}
|
||||
|
||||
function __wbg_adapter_57(arg0, arg1, arg2) {
|
||||
try {
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hb3016357f235b290(arg0, arg1, addBorrowedObject(arg2));
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h1231c314c8a7204e(arg0, arg1, addBorrowedObject(arg2));
|
||||
} finally {
|
||||
heap[stack_pointer++] = undefined;
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ function __wbg_adapter_57(arg0, arg1, arg2) {
|
|||
|
||||
/**
|
||||
* This is the entry point of the web front end. This triggers the manager app to load and begin
|
||||
* it's event loop.
|
||||
* its event loop.
|
||||
*/
|
||||
export function run_app() {
|
||||
try {
|
||||
|
@ -350,22 +350,6 @@ function __wbg_get_imports() {
|
|||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
};
|
||||
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
|
||||
const ret = getObject(arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||
const obj = takeObject(arg0).original;
|
||||
if (obj.cnt-- == 1) {
|
||||
obj.a = 0;
|
||||
return true;
|
||||
}
|
||||
const ret = false;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_modalhidebyid_a36f33eb8222a059 = function(arg0, arg1) {
|
||||
modal_hide_by_id(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
imports.wbg.__wbindgen_boolean_get = function(arg0) {
|
||||
const v = getObject(arg0);
|
||||
const ret = typeof(v) === 'boolean' ? (v ? 1 : 0) : 2;
|
||||
|
@ -410,6 +394,19 @@ function __wbg_get_imports() {
|
|||
const ret = getObject(arg0) === undefined;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||
const obj = takeObject(arg0).original;
|
||||
if (obj.cnt-- == 1) {
|
||||
obj.a = 0;
|
||||
return true;
|
||||
}
|
||||
const ret = false;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
|
||||
const ret = getObject(arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
|
||||
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
|
@ -430,6 +427,17 @@ function __wbg_get_imports() {
|
|||
const ret = setInterval(getObject(arg0), arg1);
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_modalhidebyid_5fcddcb8c6ea894f = function(arg0, arg1) {
|
||||
modal_hide_by_id(getStringFromWasm0(arg0, arg1));
|
||||
};
|
||||
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
|
||||
getObject(arg0).__yew_listener_id = arg1 >>> 0;
|
||||
};
|
||||
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).__yew_listener_id;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
|
||||
};
|
||||
imports.wbg.__wbg_subtreeid_e348577f7ef777e3 = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).__yew_subtree_id;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
|
||||
|
@ -446,14 +454,6 @@ function __wbg_get_imports() {
|
|||
imports.wbg.__wbg_setcachekey_80183b7cfc421143 = function(arg0, arg1) {
|
||||
getObject(arg0).__yew_subtree_cache_key = arg1 >>> 0;
|
||||
};
|
||||
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).__yew_listener_id;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
|
||||
};
|
||||
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
|
||||
getObject(arg0).__yew_listener_id = arg1 >>> 0;
|
||||
};
|
||||
imports.wbg.__wbg_new_abda76e883ba8a5f = function() {
|
||||
const ret = new Error();
|
||||
return addHeapObject(ret);
|
||||
|
@ -484,9 +484,6 @@ function __wbg_get_imports() {
|
|||
const ret = arg0;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_set_20cbc34131e76824 = function(arg0, arg1, arg2) {
|
||||
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
|
||||
};
|
||||
imports.wbg.__wbg_getwithrefkey_5e6d9547403deab8 = function(arg0, arg1) {
|
||||
const ret = getObject(arg0)[getObject(arg1)];
|
||||
return addHeapObject(ret);
|
||||
|
@ -504,16 +501,14 @@ function __wbg_get_imports() {
|
|||
wasm.__wbindgen_free(arg0, arg1 * 4);
|
||||
console.error(...v0);
|
||||
};
|
||||
imports.wbg.__wbg_log_1f7f93998ab961f7 = function(arg0, arg1) {
|
||||
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
|
||||
wasm.__wbindgen_free(arg0, arg1 * 4);
|
||||
console.log(...v0);
|
||||
};
|
||||
imports.wbg.__wbg_warn_0b90a269a514ae1d = function(arg0, arg1) {
|
||||
var v0 = getArrayJsValueFromWasm0(arg0, arg1).slice();
|
||||
wasm.__wbindgen_free(arg0, arg1 * 4);
|
||||
console.warn(...v0);
|
||||
};
|
||||
imports.wbg.__wbg_set_20cbc34131e76824 = function(arg0, arg1, arg2) {
|
||||
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
|
||||
};
|
||||
imports.wbg.__wbg_documentURI_4bff51077cdeeac1 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).documentURI;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
|
@ -545,44 +540,6 @@ function __wbg_get_imports() {
|
|||
const ret = getObject(arg0).querySelector(getStringFromWasm0(arg1, arg2));
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_instanceof_Window_9029196b662bc42a = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof Window;
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_document_f7ace2b956f30a4f = function(arg0) {
|
||||
const ret = getObject(arg0).document;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_location_56243dba507f472d = function(arg0) {
|
||||
const ret = getObject(arg0).location;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_history_3c2280e6b2a9316e = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).history;
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_navigator_7c9103698acde322 = function(arg0) {
|
||||
const ret = getObject(arg0).navigator;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_localStorage_dbac11bd189e9fa0 = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).localStorage;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_sessionStorage_3b863b6e15dd2bdc = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).sessionStorage;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_fetch_336b6f0cb426b46e = function(arg0, arg1) {
|
||||
const ret = getObject(arg0).fetch(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_Element_4622f5da1249a3eb = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
|
@ -624,69 +581,43 @@ function __wbg_get_imports() {
|
|||
imports.wbg.__wbg_setAttribute_e7e80b478b7b8b2f = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).setAttribute(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_state_745dc4814d321eb3 = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).state;
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_pushState_1145414a47c0b629 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
|
||||
getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_getItem_ed8e218e51f1efeb = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3));
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_removeItem_02359267b311cb85 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
getObject(arg0).removeItem(getStringFromWasm0(arg1, arg2));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_setItem_d002ee486462bfff = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_2e9aab260014946d = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3));
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_set_b34caba58723c454 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_href_47b90f0ddf3ddcd7 = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).href;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_HtmlInputElement_31b50e0cf542c524 = function(arg0) {
|
||||
imports.wbg.__wbg_instanceof_Window_9029196b662bc42a = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof HTMLInputElement;
|
||||
result = getObject(arg0) instanceof Window;
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_checked_5ccb3a66eb054121 = function(arg0) {
|
||||
const ret = getObject(arg0).checked;
|
||||
return ret;
|
||||
imports.wbg.__wbg_document_f7ace2b956f30a4f = function(arg0) {
|
||||
const ret = getObject(arg0).document;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_setchecked_e5a50baea447b8a8 = function(arg0, arg1) {
|
||||
getObject(arg0).checked = arg1 !== 0;
|
||||
imports.wbg.__wbg_location_56243dba507f472d = function(arg0) {
|
||||
const ret = getObject(arg0).location;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_value_9423da9d988ee8cf = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).value;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
imports.wbg.__wbg_history_3c2280e6b2a9316e = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).history;
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_navigator_7c9103698acde322 = function(arg0) {
|
||||
const ret = getObject(arg0).navigator;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_setvalue_1f95e61cbc382f7f = function(arg0, arg1, arg2) {
|
||||
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
|
||||
imports.wbg.__wbg_localStorage_dbac11bd189e9fa0 = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).localStorage;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_sessionStorage_3b863b6e15dd2bdc = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).sessionStorage;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_fetch_336b6f0cb426b46e = function(arg0, arg1) {
|
||||
const ret = getObject(arg0).fetch(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_HtmlElement_6f4725d4677c7968 = function(arg0) {
|
||||
let result;
|
||||
|
@ -701,6 +632,48 @@ function __wbg_get_imports() {
|
|||
imports.wbg.__wbg_focus_dbcbbbb2a04c0e1f = function() { return handleError(function (arg0) {
|
||||
getObject(arg0).focus();
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_href_d62a28e4fc1ab948 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).href;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_sethref_e5626365d7354fea = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
getObject(arg0).href = getStringFromWasm0(arg1, arg2);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_pathname_c8fd5c498079312d = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).pathname;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_search_6c3c472e076ee010 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).search;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_hash_a1a795b89dda8e3d = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).hash;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_create_c7e40b6b88186cbf = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg0).create(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_state_745dc4814d321eb3 = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).state;
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_pushState_1145414a47c0b629 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
|
||||
getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_headers_b439dcff02e808e5 = function(arg0) {
|
||||
const ret = getObject(arg0).headers;
|
||||
return addHeapObject(ret);
|
||||
|
@ -709,59 +682,6 @@ function __wbg_get_imports() {
|
|||
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_log_1d3ae0273d8f4f8a = function(arg0) {
|
||||
console.log(getObject(arg0));
|
||||
};
|
||||
imports.wbg.__wbg_newwithform_368648c82279d486 = function() { return handleError(function (arg0) {
|
||||
const ret = new FormData(getObject(arg0));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_4c356dcef81d58a5 = function(arg0, arg1, arg2) {
|
||||
const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_credentials_66b6baa89eb03c21 = function(arg0) {
|
||||
const ret = getObject(arg0).credentials;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_parentNode_9e53f8b17eb98c9d = function(arg0) {
|
||||
const ret = getObject(arg0).parentNode;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_parentElement_c75962bc9997ea5f = function(arg0) {
|
||||
const ret = getObject(arg0).parentElement;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_lastChild_0cee692010bac6c2 = function(arg0) {
|
||||
const ret = getObject(arg0).lastChild;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_nextSibling_304d9aac7c2774ae = function(arg0) {
|
||||
const ret = getObject(arg0).nextSibling;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_setnodeValue_d1c8382910b45e04 = function(arg0, arg1, arg2) {
|
||||
getObject(arg0).nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
|
||||
};
|
||||
imports.wbg.__wbg_textContent_c5d9e21ee03c63d4 = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).textContent;
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
};
|
||||
imports.wbg.__wbg_appendChild_51339d4cde00ee22 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg0).appendChild(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_insertBefore_ffa01d4b747c95fc = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_removeChild_973429f368206138 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg0).removeChild(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_instanceof_Response_fc4327dbfcdf5ced = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
|
@ -784,78 +704,18 @@ function __wbg_get_imports() {
|
|||
const ret = getObject(arg0).json();
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_create_c7e40b6b88186cbf = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg0).create(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_e66794f89dcd7828 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg0).get(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_add_3eafedc4b2a28db0 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_remove_8ae45e50cb58bb66 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_target_f171e89c61e2bccf = function(arg0) {
|
||||
const ret = getObject(arg0).target;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_bubbles_63572b91f3885ef1 = function(arg0) {
|
||||
const ret = getObject(arg0).bubbles;
|
||||
imports.wbg.__wbg_instanceof_ShadowRoot_b64337370f59fe2d = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof ShadowRoot;
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_cancelBubble_90d1c3aa2a76cbeb = function(arg0) {
|
||||
const ret = getObject(arg0).cancelBubble;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_composedPath_cf1bb5b8bcff496f = function(arg0) {
|
||||
const ret = getObject(arg0).composedPath();
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_preventDefault_24104f3f0a54546a = function(arg0) {
|
||||
getObject(arg0).preventDefault();
|
||||
};
|
||||
imports.wbg.__wbg_addEventListener_a5963e26cd7b176b = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_removeEventListener_782040b4432709cb = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_href_d62a28e4fc1ab948 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).href;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_pathname_c8fd5c498079312d = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).pathname;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_search_6c3c472e076ee010 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).search;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_hash_a1a795b89dda8e3d = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).hash;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_replace_5d1d2b7956cafd7b = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
getObject(arg0).replace(getStringFromWasm0(arg1, arg2));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_getClientExtensionResults_b9108fbba9f54b38 = function(arg0) {
|
||||
const ret = getObject(arg0).getClientExtensionResults();
|
||||
imports.wbg.__wbg_host_e1c47c33975060d3 = function(arg0) {
|
||||
const ret = getObject(arg0).host;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_href_17ed54b321396524 = function(arg0, arg1) {
|
||||
|
@ -900,29 +760,62 @@ function __wbg_get_imports() {
|
|||
const ret = new URL(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_instanceof_HtmlFormElement_b57527983c7c1ada = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof HTMLFormElement;
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_ShadowRoot_b64337370f59fe2d = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof ShadowRoot;
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_host_e1c47c33975060d3 = function(arg0) {
|
||||
const ret = getObject(arg0).host;
|
||||
imports.wbg.__wbg_newwithform_368648c82279d486 = function() { return handleError(function (arg0) {
|
||||
const ret = new FormData(getObject(arg0));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_4c356dcef81d58a5 = function(arg0, arg1, arg2) {
|
||||
const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_instanceof_HtmlInputElement_31b50e0cf542c524 = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof HTMLInputElement;
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_setchecked_e5a50baea447b8a8 = function(arg0, arg1) {
|
||||
getObject(arg0).checked = arg1 !== 0;
|
||||
};
|
||||
imports.wbg.__wbg_value_9423da9d988ee8cf = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).value;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
};
|
||||
imports.wbg.__wbg_setvalue_1f95e61cbc382f7f = function(arg0, arg1, arg2) {
|
||||
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
|
||||
};
|
||||
imports.wbg.__wbg_target_f171e89c61e2bccf = function(arg0) {
|
||||
const ret = getObject(arg0).target;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_bubbles_63572b91f3885ef1 = function(arg0) {
|
||||
const ret = getObject(arg0).bubbles;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_cancelBubble_90d1c3aa2a76cbeb = function(arg0) {
|
||||
const ret = getObject(arg0).cancelBubble;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_composedPath_cf1bb5b8bcff496f = function(arg0) {
|
||||
const ret = getObject(arg0).composedPath();
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_preventDefault_24104f3f0a54546a = function(arg0) {
|
||||
getObject(arg0).preventDefault();
|
||||
};
|
||||
imports.wbg.__wbg_href_47b90f0ddf3ddcd7 = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).href;
|
||||
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
};
|
||||
imports.wbg.__wbg_value_3c5f08ffc2b7d6f9 = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).value;
|
||||
|
@ -934,6 +827,97 @@ function __wbg_get_imports() {
|
|||
imports.wbg.__wbg_setvalue_0dc100d4b9908028 = function(arg0, arg1, arg2) {
|
||||
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
|
||||
};
|
||||
imports.wbg.__wbg_getClientExtensionResults_b9108fbba9f54b38 = function(arg0) {
|
||||
const ret = getObject(arg0).getClientExtensionResults();
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_getItem_ed8e218e51f1efeb = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = getObject(arg1).getItem(getStringFromWasm0(arg2, arg3));
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_removeItem_02359267b311cb85 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
getObject(arg0).removeItem(getStringFromWasm0(arg1, arg2));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_setItem_d002ee486462bfff = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).setItem(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_addEventListener_a5963e26cd7b176b = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_removeEventListener_782040b4432709cb = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_instanceof_HtmlFormElement_b57527983c7c1ada = function(arg0) {
|
||||
let result;
|
||||
try {
|
||||
result = getObject(arg0) instanceof HTMLFormElement;
|
||||
} catch {
|
||||
result = false;
|
||||
}
|
||||
const ret = result;
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_credentials_66b6baa89eb03c21 = function(arg0) {
|
||||
const ret = getObject(arg0).credentials;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_parentNode_9e53f8b17eb98c9d = function(arg0) {
|
||||
const ret = getObject(arg0).parentNode;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_parentElement_c75962bc9997ea5f = function(arg0) {
|
||||
const ret = getObject(arg0).parentElement;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_lastChild_0cee692010bac6c2 = function(arg0) {
|
||||
const ret = getObject(arg0).lastChild;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_nextSibling_304d9aac7c2774ae = function(arg0) {
|
||||
const ret = getObject(arg0).nextSibling;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbg_setnodeValue_d1c8382910b45e04 = function(arg0, arg1, arg2) {
|
||||
getObject(arg0).nodeValue = arg1 === 0 ? undefined : getStringFromWasm0(arg1, arg2);
|
||||
};
|
||||
imports.wbg.__wbg_textContent_c5d9e21ee03c63d4 = function(arg0, arg1) {
|
||||
const ret = getObject(arg1).textContent;
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
};
|
||||
imports.wbg.__wbg_appendChild_51339d4cde00ee22 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg0).appendChild(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_insertBefore_ffa01d4b747c95fc = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
const ret = getObject(arg0).insertBefore(getObject(arg1), getObject(arg2));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_removeChild_973429f368206138 = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg0).removeChild(getObject(arg1));
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_add_3eafedc4b2a28db0 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_remove_8ae45e50cb58bb66 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_2e9aab260014946d = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||
const ret = getObject(arg1).get(getStringFromWasm0(arg2, arg3));
|
||||
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
var len1 = WASM_VECTOR_LEN;
|
||||
getInt32Memory0()[arg0 / 4 + 1] = len1;
|
||||
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_set_b34caba58723c454 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
|
||||
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_get_44be0491f933a435 = function(arg0, arg1) {
|
||||
const ret = getObject(arg0)[arg1 >>> 0];
|
||||
return addHeapObject(ret);
|
||||
|
@ -1146,20 +1130,20 @@ function __wbg_get_imports() {
|
|||
const ret = wasm.memory;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper659 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 380, __wbg_adapter_48);
|
||||
imports.wbg.__wbindgen_closure_wrapper1151 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 587, __wbg_adapter_48);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper4175 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1999, __wbg_adapter_51);
|
||||
imports.wbg.__wbindgen_closure_wrapper3677 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1715, __wbg_adapter_51);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper4941 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 2306, __wbg_adapter_54);
|
||||
imports.wbg.__wbindgen_closure_wrapper3753 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1744, __wbg_adapter_54);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper5000 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 2330, __wbg_adapter_57);
|
||||
imports.wbg.__wbindgen_closure_wrapper3837 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1781, __wbg_adapter_57);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
||||
|
@ -1203,7 +1187,7 @@ async function __wbg_init(input) {
|
|||
if (wasm !== undefined) return wasm;
|
||||
|
||||
if (typeof input === 'undefined') {
|
||||
input = new URL('kanidmd_web_ui_bg.wasm', import.meta.url);
|
||||
input = new URL('kanidmd_web_ui_user_bg.wasm', import.meta.url);
|
||||
}
|
||||
const imports = __wbg_get_imports();
|
||||
|
BIN
server/web_ui/pkg/kanidmd_web_ui_user.js.br
Normal file
BIN
server/web_ui/pkg/kanidmd_web_ui_user.js.br
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/kanidmd_web_ui_user_bg.wasm
Normal file
BIN
server/web_ui/pkg/kanidmd_web_ui_user_bg.wasm
Normal file
Binary file not shown.
BIN
server/web_ui/pkg/kanidmd_web_ui_user_bg.wasm.br
Normal file
BIN
server/web_ui/pkg/kanidmd_web_ui_user_bg.wasm.br
Normal file
Binary file not shown.
6
server/web_ui/pkg/shared.js
Normal file
6
server/web_ui/pkg/shared.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
// This is easier to have in JS than in WASM
|
||||
export function modal_hide_by_id(m) {
|
||||
var elem = document.getElementById(m);
|
||||
var modal = bootstrap.Modal.getInstance(elem);
|
||||
modal.hide();
|
||||
};
|
|
@ -129,6 +129,13 @@ body {
|
|||
right: 1rem;
|
||||
}
|
||||
|
||||
.navbar-toggler-img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.navbar .form-control {
|
||||
padding: 0.75rem 1rem;
|
||||
border-width: 0;
|
||||
|
@ -236,3 +243,8 @@ body {
|
|||
-o-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.oauth2-img {
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
}
|
7
server/web_ui/pkg/wasmloader_admin.js
Normal file
7
server/web_ui/pkg/wasmloader_admin.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// loads the module which loads the WASM. It's loaders all the way down.
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui_admin.js';
|
||||
async function main() {
|
||||
await init('/pkg/kanidmd_web_ui_admin_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
|
@ -1,7 +1,7 @@
|
|||
// loads the module which loads the WASM. It's loaders all the way down.
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui.js';
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui_login_flows.js';
|
||||
async function main() {
|
||||
await init('/pkg/kanidmd_web_ui_bg.wasm');
|
||||
await init('/pkg/kanidmd_web_ui_login_flows_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
7
server/web_ui/pkg/wasmloader_user.js
Normal file
7
server/web_ui/pkg/wasmloader_user.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
// loads the module which loads the WASM. It's loaders all the way down.
|
||||
import init, { run_app } from '/pkg/kanidmd_web_ui_user.js';
|
||||
async function main() {
|
||||
await init('/pkg/kanidmd_web_ui_user_bg.wasm');
|
||||
run_app();
|
||||
}
|
||||
main()
|
68
server/web_ui/shared/Cargo.toml
Normal file
68
server/web_ui/shared/Cargo.toml
Normal file
|
@ -0,0 +1,68 @@
|
|||
[package]
|
||||
name = "kanidmd_web_ui_shared"
|
||||
description = "Kanidm Server Web UI - Shared Library"
|
||||
documentation = "https://docs.rs/kanidm/latest/kanidm/"
|
||||
|
||||
version = { workspace = true }
|
||||
authors = [
|
||||
"William Brown <william@blackhats.net.au>",
|
||||
"James Hodgkinson <james@terminaloutcomes.com>",
|
||||
]
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
homepage = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
|
||||
# [lib]
|
||||
# crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
gloo = { workspace = true }
|
||||
js-sys = { workspace = true }
|
||||
kanidm_proto = { workspace = true, features = ["wasm"] }
|
||||
lazy_static.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde-wasm-bindgen = { workspace = true }
|
||||
time = { workspace = true }
|
||||
url = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
wasm-bindgen = { workspace = true }
|
||||
wasm-bindgen-futures = { workspace = true }
|
||||
yew = { workspace = true, features = ["csr"] }
|
||||
yew-router = { workspace = true }
|
||||
|
||||
[dependencies.web-sys]
|
||||
workspace = true
|
||||
features = [
|
||||
# "AuthenticationExtensionsClientOutputs",
|
||||
# "AuthenticatorResponse",
|
||||
# "CredentialCreationOptions",
|
||||
# "CredentialRequestOptions",
|
||||
# "CredentialsContainer",
|
||||
"DomTokenList",
|
||||
"Element",
|
||||
"Event",
|
||||
"FocusEvent",
|
||||
"FormData",
|
||||
"Headers",
|
||||
"HtmlButtonElement",
|
||||
"HtmlDocument",
|
||||
"HtmlFormElement",
|
||||
"Navigator",
|
||||
# "PublicKeyCredential",
|
||||
# "PublicKeyCredentialCreationOptions",
|
||||
# "PublicKeyCredentialRpEntity",
|
||||
# "PublicKeyCredentialUserEntity",
|
||||
"Request",
|
||||
# "RequestCredentials",
|
||||
# "RequestInit",
|
||||
# "RequestMode",
|
||||
# "RequestRedirect",
|
||||
"Response",
|
||||
"Window",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = { workspace = true }
|
|
@ -15,6 +15,7 @@ pub const ID_SIGNOUTMODAL: &str = "signoutModal";
|
|||
pub const ID_UNIX_PASSWORDCHANGE: &str = "unixPasswordModal";
|
||||
pub const ID_IDENTITY_VERIFICATION_SYSTEM_TOTP_MODAL: &str = "identityVerificationSystemTotpModal";
|
||||
pub const ID_CRED_RESET_CODE: &str = "credResetCodeModal";
|
||||
pub const ID_NAVBAR_COLLAPSE: &str = "navbarCollapse";
|
||||
// classes for buttons
|
||||
pub const CLASS_BUTTON_DARK: &str = "btn btn-dark";
|
||||
pub const CLASS_BUTTON_SUCCESS: &str = "btn btn-success";
|
||||
|
@ -32,9 +33,29 @@ pub const CSS_CELL: &str = "p-1";
|
|||
|
||||
pub const CSS_DT: &str = "col-6";
|
||||
|
||||
pub const CSS_BREADCRUMB_ITEM: &str = "breadcrumb-item";
|
||||
pub const CSS_BREADCRUMB_ITEM_ACTIVE: &str = "breadcrumb-item active";
|
||||
// pub const CSS_BREADCRUMB_ITEM: &str = "breadcrumb-item";
|
||||
// pub const CSS_BREADCRUMB_ITEM_ACTIVE: &str = "breadcrumb-item active";
|
||||
|
||||
// used in the UI for ... cards
|
||||
pub const CSS_CARD: &str = "card text-center";
|
||||
pub const CSS_CARD_BODY: &str = "card-body text-center";
|
||||
|
||||
pub const CSS_NAV_LINK: &str = "nav-link";
|
||||
|
||||
pub const CSS_ALERT_WARNING: &str = "alert alert-warning";
|
||||
pub const CSS_ALERT_DANGER: &str = "alert alert-danger";
|
||||
|
||||
pub const CSS_NAVBAR_NAV: &str = "navbar navbar-expand-md navbar-dark bg-dark mb-4";
|
||||
pub const CSS_NAVBAR_BRAND: &str = "navbar-brand navbar-dark";
|
||||
pub const CSS_NAVBAR_LINKS_UL: &str = "navbar-nav me-auto mb-2 mb-md-0";
|
||||
|
||||
pub const URL_ADMIN: &str = "/ui/admin";
|
||||
pub const URL_OAUTH2: &str = "/ui/oauth2";
|
||||
pub const URL_USER_HOME: &str = "/ui/apps";
|
||||
pub const URL_USER_PROFILE: &str = "/ui/profile";
|
||||
pub const URL_LOGIN: &str = "/ui/login";
|
||||
pub const URL_REAUTH: &str = "/ui/reauth";
|
||||
pub const URL_RESET: &str = "/ui/reset";
|
||||
|
||||
pub const IMG_FAVICON: &str = "/pkg/img/favicon.png";
|
||||
pub const IMG_LOGO_SQUARE: &str = "/pkg/img/logo-square.svg";
|
183
server/web_ui/shared/src/lib.rs
Normal file
183
server/web_ui/shared/src/lib.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
use constants::CONTENT_TYPE;
|
||||
use error::FetchError;
|
||||
use gloo::console;
|
||||
|
||||
use kanidm_proto::constants::uri::V1_AUTH_VALID;
|
||||
use kanidm_proto::constants::KOPID;
|
||||
use kanidm_proto::constants::{APPLICATION_JSON, KSESSIONID};
|
||||
use models::{clear_bearer_token, get_bearer_token};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Headers, Request, RequestInit, RequestMode, Response};
|
||||
|
||||
use gloo::storage::{SessionStorage as TemporaryStorage, Storage};
|
||||
use yew::{html, Html};
|
||||
|
||||
use crate::constants::{CSS_ALERT_WARNING, IMG_LOGO_SQUARE};
|
||||
|
||||
pub mod constants;
|
||||
pub mod error;
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
pub mod models;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
|
||||
const AUTH_SESSION_ID: &str = "auth_session_id";
|
||||
|
||||
pub fn pop_auth_session_id() -> Option<String> {
|
||||
let l: Result<String, _> = TemporaryStorage::get(AUTH_SESSION_ID);
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(format!("auth_session_id -> {:?}", l).as_str());
|
||||
TemporaryStorage::delete(AUTH_SESSION_ID);
|
||||
l.ok()
|
||||
}
|
||||
|
||||
pub fn push_auth_session_id(r: String) {
|
||||
TemporaryStorage::set(AUTH_SESSION_ID, r).expect_throw(&format!(
|
||||
"failed to set {} in temporary storage",
|
||||
AUTH_SESSION_ID
|
||||
));
|
||||
}
|
||||
|
||||
/// Build and send a request to the backend, with some standard headers and pull back
|
||||
/// (kopid, status, json, headers)
|
||||
pub async fn do_request(
|
||||
uri: &str,
|
||||
method: RequestMethod,
|
||||
body: Option<JsValue>,
|
||||
) -> Result<(Option<String>, u16, JsValue, Headers), FetchError> {
|
||||
let mut opts = RequestInit::new();
|
||||
opts.method(&method.to_string());
|
||||
opts.mode(RequestMode::SameOrigin);
|
||||
opts.credentials(web_sys::RequestCredentials::SameOrigin);
|
||||
|
||||
if let Some(body) = body {
|
||||
#[cfg(debug_assertions)]
|
||||
if method == RequestMethod::GET {
|
||||
gloo::console::debug!("This seems odd, you've supplied a body with a GET request?")
|
||||
}
|
||||
opts.body(Some(&body));
|
||||
}
|
||||
|
||||
let request = Request::new_with_str_and_init(uri, &opts)?;
|
||||
request
|
||||
.headers()
|
||||
.set(CONTENT_TYPE, APPLICATION_JSON)
|
||||
.expect_throw("failed to set content-type header");
|
||||
|
||||
if let Some(sessionid) = pop_auth_session_id() {
|
||||
request
|
||||
.headers()
|
||||
.set(KSESSIONID, &sessionid)
|
||||
.expect_throw(&format!("failed to set {} header", KSESSIONID));
|
||||
}
|
||||
|
||||
if let Some(bearer_token) = get_bearer_token() {
|
||||
request
|
||||
.headers()
|
||||
.set("authorization", &bearer_token)
|
||||
.expect_throw("failed to set authorization header");
|
||||
}
|
||||
|
||||
let window = utils::window();
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||
let status = resp.status();
|
||||
let headers: Headers = resp.headers();
|
||||
|
||||
if let Some(sessionid) = headers.get(KSESSIONID).ok().flatten() {
|
||||
push_auth_session_id(sessionid);
|
||||
}
|
||||
|
||||
let kopid = headers.get(KOPID).ok().flatten();
|
||||
|
||||
let body = match resp.json() {
|
||||
Ok(json_future) => match JsFuture::from(json_future).await {
|
||||
Ok(body) => body,
|
||||
Err(e) => {
|
||||
let e_msg = format!("future json error -> {:?}", e);
|
||||
console::error!(e_msg.as_str());
|
||||
JsValue::NULL
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let e_msg = format!("response json error -> {:?}", e);
|
||||
console::error!(e_msg.as_str());
|
||||
JsValue::NULL
|
||||
}
|
||||
};
|
||||
|
||||
Ok((kopid, status, body, headers))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum RequestMethod {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
}
|
||||
|
||||
impl ToString for RequestMethod {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
RequestMethod::PUT => "PUT".to_string(),
|
||||
RequestMethod::POST => "POST".to_string(),
|
||||
RequestMethod::GET => "GET".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// creates the "Kanidm is alpha" banner
|
||||
pub fn alpha_warning_banner() -> Html {
|
||||
html!(
|
||||
<div class={CSS_ALERT_WARNING} role="alert">
|
||||
{"🦀 Kanidm is still in early Alpha, this interface is a placeholder! "}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a HTML img tag with the Kanidm logo
|
||||
pub fn logo_img() -> Html {
|
||||
html! {
|
||||
<img src={IMG_LOGO_SQUARE} alt="Kanidm" class="kanidm_logo"/>
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SessionStatus {
|
||||
TokenValid,
|
||||
LoginRequired,
|
||||
Error { emsg: String, kopid: Option<String> },
|
||||
}
|
||||
|
||||
impl ToString for SessionStatus {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
SessionStatus::TokenValid => "SessionStatus::TokenValid".to_string(),
|
||||
SessionStatus::LoginRequired => "SessionStatus::LoginRequired".to_string(),
|
||||
SessionStatus::Error { emsg, kopid } => {
|
||||
format!("SessionStatus::Error: {} {:?}", emsg, kopid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that the current stored session token is valid
|
||||
pub async fn fetch_session_valid() -> Result<SessionStatus, FetchError> {
|
||||
let (kopid, status, value, _) = do_request(V1_AUTH_VALID, RequestMethod::GET, None).await?;
|
||||
|
||||
if status == 200 {
|
||||
Ok(SessionStatus::TokenValid)
|
||||
} else if status == 401 {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("Session token is invalid, clearing it");
|
||||
clear_bearer_token();
|
||||
Ok(SessionStatus::LoginRequired)
|
||||
} else {
|
||||
let emsg = value.as_string().unwrap_or_default();
|
||||
Ok(SessionStatus::Error { emsg, kopid })
|
||||
}
|
||||
}
|
120
server/web_ui/shared/src/models.rs
Normal file
120
server/web_ui/shared/src/models.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
#[cfg(debug_assertions)]
|
||||
use gloo::console;
|
||||
use gloo::storage::{LocalStorage, SessionStorage as TemporaryStorage, Storage};
|
||||
use kanidm_proto::oauth2::AuthorisationRequest;
|
||||
use kanidm_proto::v1::{CUSessionToken, CUStatus};
|
||||
use wasm_bindgen::UnwrapThrowExt;
|
||||
|
||||
use crate::constants::URL_USER_HOME;
|
||||
|
||||
const BEARER_TOKEN: &str = "bearer_token";
|
||||
const CRED_UPDATE_SESSION: &str = "cred_update_session";
|
||||
const LOGIN_HINT: &str = "login_hint";
|
||||
const LOGIN_REMEMBER_ME: &str = "login_remember_me";
|
||||
const RETURN_LOCATION: &str = "return_location";
|
||||
const OAUTH2_AUTHORIZATION_REQUEST: &str = "oauth2_authorisation_request";
|
||||
|
||||
/// Store the bearer token `r` in local storage
|
||||
pub fn set_bearer_token(r: String) {
|
||||
LocalStorage::set(BEARER_TOKEN, r).expect_throw(&format!("failed to set {}", BEARER_TOKEN));
|
||||
}
|
||||
|
||||
pub fn get_bearer_token() -> Option<String> {
|
||||
let l: Result<String, _> = LocalStorage::get(BEARER_TOKEN);
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(&format!(
|
||||
"login_hint::get_login_remember_me -> present={:?}",
|
||||
l.is_ok()
|
||||
));
|
||||
l.ok()
|
||||
}
|
||||
|
||||
pub fn clear_bearer_token() {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("clearing the bearer token from local storage");
|
||||
LocalStorage::delete(BEARER_TOKEN);
|
||||
}
|
||||
|
||||
/// Keep the "return location" in temporary storage when we're planning to do a redirect
|
||||
pub fn push_return_location(l: &str) {
|
||||
TemporaryStorage::set(RETURN_LOCATION, l).expect_throw(&format!(
|
||||
"failed to set {} in temporary storage",
|
||||
RETURN_LOCATION
|
||||
));
|
||||
}
|
||||
|
||||
/// We keep the "return location" in temporary storage when we're planning to do a redirect,
|
||||
/// this pulls it back and removes it from storage.
|
||||
pub fn pop_return_location() -> String {
|
||||
let l: Result<String, _> = TemporaryStorage::get(RETURN_LOCATION);
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(format!("{} -> {:?}", RETURN_LOCATION, l).as_str());
|
||||
TemporaryStorage::delete(RETURN_LOCATION);
|
||||
l.unwrap_or(URL_USER_HOME.to_string())
|
||||
}
|
||||
|
||||
/// Store the user's username in temporary storage when we're passing it around.
|
||||
pub fn push_login_hint(username: String) {
|
||||
TemporaryStorage::set(LOGIN_HINT, username).expect_throw("failed to set login hint");
|
||||
}
|
||||
|
||||
pub fn get_login_hint() -> Option<String> {
|
||||
let l: Result<String, _> = TemporaryStorage::get(LOGIN_HINT);
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(format!("login_hint::get_login_hint -> {:?}", l).as_str());
|
||||
l.ok()
|
||||
}
|
||||
|
||||
pub fn pop_login_hint() -> Option<String> {
|
||||
let l: Result<String, _> = TemporaryStorage::get(LOGIN_HINT);
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(format!("login_hint::pop_login_hint -> {:?}", l).as_str());
|
||||
TemporaryStorage::delete(LOGIN_HINT);
|
||||
l.ok()
|
||||
}
|
||||
|
||||
/// Keep track of the user's username when they set the "remember me" flag on the UI
|
||||
pub fn push_login_remember_me(username: String) {
|
||||
LocalStorage::set(LOGIN_REMEMBER_ME, username).expect_throw("failed to set login remember me");
|
||||
}
|
||||
|
||||
pub fn get_login_remember_me() -> Option<String> {
|
||||
let username: Result<String, _> = LocalStorage::get(LOGIN_REMEMBER_ME);
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(format!("login_hint::get_login_remember_me -> {:?}", username).as_str());
|
||||
username.ok()
|
||||
}
|
||||
|
||||
pub fn pop_login_remember_me() -> Option<String> {
|
||||
let username: Result<String, _> = LocalStorage::get(LOGIN_REMEMBER_ME);
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(format!("login_hint::pop_login_remember_me -> {:?}", username).as_str());
|
||||
LocalStorage::delete(LOGIN_REMEMBER_ME);
|
||||
username.ok()
|
||||
}
|
||||
|
||||
pub fn push_oauth2_authorisation_request(r: AuthorisationRequest) {
|
||||
TemporaryStorage::set(OAUTH2_AUTHORIZATION_REQUEST, r).expect_throw(&format!(
|
||||
"failed to set {} in temporary storage",
|
||||
OAUTH2_AUTHORIZATION_REQUEST
|
||||
));
|
||||
}
|
||||
|
||||
pub fn pop_oauth2_authorisation_request() -> Option<AuthorisationRequest> {
|
||||
let l: Result<AuthorisationRequest, _> = TemporaryStorage::get(OAUTH2_AUTHORIZATION_REQUEST);
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!(format!("{} -> {:?}", OAUTH2_AUTHORIZATION_REQUEST, l).as_str());
|
||||
TemporaryStorage::delete(OAUTH2_AUTHORIZATION_REQUEST);
|
||||
l.ok()
|
||||
}
|
||||
|
||||
/// Pushes the "cred_update_session" element into the browser's temporary storage
|
||||
pub fn push_cred_update_session(s: (CUSessionToken, CUStatus)) {
|
||||
TemporaryStorage::set(CRED_UPDATE_SESSION, s).expect_throw("failed to set cred session token");
|
||||
}
|
||||
|
||||
/// Pulls the "cred_update_session" element from the browser's temporary storage
|
||||
pub fn get_cred_update_session() -> Option<(CUSessionToken, CUStatus)> {
|
||||
let l: Result<(CUSessionToken, CUStatus), _> = TemporaryStorage::get(CRED_UPDATE_SESSION);
|
||||
l.ok()
|
||||
}
|
70
server/web_ui/shared/src/ui.rs
Normal file
70
server/web_ui/shared/src/ui.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
//! UI things
|
||||
//!
|
||||
|
||||
use gloo::console;
|
||||
use yew::{html, BaseComponent, Context, Html};
|
||||
|
||||
use crate::constants::{CSS_NAV_LINK, ID_SIGNOUTMODAL};
|
||||
use crate::models::clear_bearer_token;
|
||||
use crate::{do_request, RequestMethod};
|
||||
|
||||
/// returns an a-href link which can trigger the signout flow
|
||||
pub fn signout_link() -> Html {
|
||||
html! {
|
||||
<a class={CSS_NAV_LINK} href="#" data-bs-toggle="modal"
|
||||
data-bs-target={["#", ID_SIGNOUTMODAL].concat()}
|
||||
>{"Sign out"}</a>
|
||||
}
|
||||
}
|
||||
|
||||
/// does the logout action, calling the api and clearing the local tokens
|
||||
pub async fn ui_logout() -> Result<(), (String, Option<String>)> {
|
||||
let (kopid, status, value, _) = do_request("/v1/logout", RequestMethod::GET, None)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let emsg = format!("failed to logout -> {:?}", e);
|
||||
console::error!(emsg.as_str());
|
||||
(emsg, None)
|
||||
})?;
|
||||
|
||||
if status == 200 {
|
||||
// only clear the local token if it actually worked, because otherwise you could
|
||||
// think the session is gone, while it's still live.
|
||||
clear_bearer_token();
|
||||
Ok(())
|
||||
} else {
|
||||
let emsg = value.as_string().unwrap_or_default();
|
||||
Err((emsg, kopid))
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the signout modal dialogue box - the "target" is the Message to send when clicked.
|
||||
pub fn signout_modal<T, U>(ctx: &Context<T>, target: U) -> Html
|
||||
where
|
||||
T: BaseComponent,
|
||||
U: Clone + 'static,
|
||||
<T as BaseComponent>::Message: From<U>,
|
||||
{
|
||||
html! {<div class="modal" tabindex="-1" role="dialog" id={ID_SIGNOUTMODAL}>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{"Confirm Sign out"}</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
{"Are you sure you'd like to log out?"}<br />
|
||||
<img src="/pkg/img/kani-waving.svg" alt="Kani waving goodbye" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target={["#", ID_SIGNOUTMODAL].concat()}
|
||||
onclick={ ctx.link().callback(move |_| target.clone()) }>{ "Sign out" }</button>
|
||||
<button type="button" class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"
|
||||
>{"Cancel"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
use gloo::console;
|
||||
|
||||
use url::Url;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::{JsCast, UnwrapThrowExt};
|
||||
|
@ -7,6 +8,8 @@ use web_sys::{Document, HtmlElement, HtmlInputElement, Window};
|
|||
use yew::virtual_dom::VNode;
|
||||
use yew::{html, Html};
|
||||
|
||||
use crate::constants::{CSS_ALERT_DANGER, CSS_PAGE_HEADER};
|
||||
|
||||
pub fn window() -> Window {
|
||||
web_sys::window().expect_throw("Unable to retrieve window")
|
||||
}
|
||||
|
@ -77,7 +80,7 @@ pub fn get_value_from_element_id(id: &str) -> Option<String> {
|
|||
.map(|element| element.value())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(raw_module = "/pkg/wasmloader.js")]
|
||||
#[wasm_bindgen(raw_module = "/pkg/shared.js")]
|
||||
extern "C" {
|
||||
pub fn modal_hide_by_id(m: &str);
|
||||
}
|
||||
|
@ -93,15 +96,18 @@ pub fn do_footer() -> VNode {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn do_alert_error(alert_title: &str, alert_message: Option<&str>) -> Html {
|
||||
pub fn do_alert_error(alert_title: &str, alert_message: Option<&str>, dismissable: bool) -> Html {
|
||||
html! {
|
||||
<div class="container">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<div class={CSS_ALERT_DANGER} role="alert">
|
||||
<p><strong>{ alert_title }</strong></p>
|
||||
if let Some(value) = alert_message {
|
||||
<p>{ value }</p>
|
||||
}
|
||||
if dismissable {
|
||||
<button type="button" class="btn btn-close" data-dismiss="alert" aria-label="Close"></button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -110,7 +116,7 @@ pub fn do_alert_error(alert_title: &str, alert_message: Option<&str>) -> Html {
|
|||
|
||||
pub fn do_page_header(page_title: &str) -> Html {
|
||||
html! {
|
||||
<div class={crate::constants::CSS_PAGE_HEADER}>
|
||||
<div class={CSS_PAGE_HEADER}>
|
||||
<h2>{ page_title }</h2>
|
||||
</div>
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue