mirror of
				https://github.com/nitnelave/lldap.git
				synced 2023-04-12 14:25:13 +00:00 
			
		
		
		
	Merge branch 'main' into devcontainer-update
This commit is contained in:
		
						commit
						76d8712d1c
					
				
							
								
								
									
										233
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										233
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							@ -352,12 +352,6 @@ version = "1.0.69"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "anymap"
 | 
			
		||||
version = "0.12.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "arrayref"
 | 
			
		||||
version = "0.3.6"
 | 
			
		||||
@ -660,12 +654,6 @@ version = "1.0.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "cfg-match"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8100e46ff92eb85bf6dc2930c73f2a4f7176393c84a9446b3d501e1b354e7b34"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "chrono"
 | 
			
		||||
version = "0.4.23"
 | 
			
		||||
@ -1524,22 +1512,40 @@ checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gloo"
 | 
			
		||||
version = "0.2.1"
 | 
			
		||||
version = "0.4.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "68ce6f2dfa9f57f15b848efa2aade5e1850dc72986b87a2b0752d44ca08f4967"
 | 
			
		||||
checksum = "23947965eee55e3e97a5cd142dd4c10631cc349b48cecca0ed230fd296f568cd"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "gloo-console-timer",
 | 
			
		||||
 "gloo-console",
 | 
			
		||||
 "gloo-dialogs",
 | 
			
		||||
 "gloo-events",
 | 
			
		||||
 "gloo-file",
 | 
			
		||||
 "gloo-render",
 | 
			
		||||
 "gloo-storage",
 | 
			
		||||
 "gloo-timers",
 | 
			
		||||
 "gloo-utils",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gloo-console-timer"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
name = "gloo-console"
 | 
			
		||||
version = "0.2.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b48675544b29ac03402c6dffc31a912f716e38d19f7e74b78b7e900ec3c941ea"
 | 
			
		||||
checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "gloo-utils",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gloo-dialogs"
 | 
			
		||||
version = "0.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -1555,26 +1561,87 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gloo-file"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
version = "0.2.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8f9fecfe46b5dc3cc46f58e98ba580cc714f2c93860796d002eb3527a465ef49"
 | 
			
		||||
checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "futures-channel",
 | 
			
		||||
 "gloo-events",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gloo-net"
 | 
			
		||||
version = "0.2.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "futures-channel",
 | 
			
		||||
 "futures-core",
 | 
			
		||||
 "futures-sink",
 | 
			
		||||
 "gloo-utils",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "pin-project",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "wasm-bindgen-futures",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gloo-render"
 | 
			
		||||
version = "0.1.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gloo-storage"
 | 
			
		||||
version = "0.2.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "gloo-utils",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gloo-timers"
 | 
			
		||||
version = "0.2.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "futures-channel",
 | 
			
		||||
 "futures-core",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "gloo-utils"
 | 
			
		||||
version = "0.1.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "graphql-introspection-query"
 | 
			
		||||
version = "0.2.0"
 | 
			
		||||
@ -2232,19 +2299,6 @@ dependencies = [
 | 
			
		||||
 "webpki-roots",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "lexical-core"
 | 
			
		||||
version = "0.7.6"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "arrayvec",
 | 
			
		||||
 "bitflags",
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "ryu",
 | 
			
		||||
 "static_assertions",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "libc"
 | 
			
		||||
version = "0.2.139"
 | 
			
		||||
@ -2361,6 +2415,9 @@ dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "base64 0.13.1",
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "gloo-console",
 | 
			
		||||
 "gloo-file",
 | 
			
		||||
 "gloo-net",
 | 
			
		||||
 "graphql_client 0.10.0",
 | 
			
		||||
 "http",
 | 
			
		||||
 "image",
 | 
			
		||||
@ -2374,12 +2431,12 @@ dependencies = [
 | 
			
		||||
 "validator",
 | 
			
		||||
 "validator_derive 0.16.0",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "wasm-bindgen-futures",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
 "yew",
 | 
			
		||||
 "yew-router",
 | 
			
		||||
 "yew_form",
 | 
			
		||||
 "yew_form_derive",
 | 
			
		||||
 "yewtil",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@ -2559,17 +2616,6 @@ version = "2.2.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "nom"
 | 
			
		||||
version = "5.1.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "lexical-core",
 | 
			
		||||
 "memchr",
 | 
			
		||||
 "version_check",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "nom"
 | 
			
		||||
version = "7.1.3"
 | 
			
		||||
@ -3260,6 +3306,12 @@ dependencies = [
 | 
			
		||||
 "winapi",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "route-recognizer"
 | 
			
		||||
version = "0.3.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rsa"
 | 
			
		||||
version = "0.6.1"
 | 
			
		||||
@ -3384,6 +3436,12 @@ dependencies = [
 | 
			
		||||
 "windows-sys 0.42.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "scoped-tls-hkt"
 | 
			
		||||
version = "0.1.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "scopeguard"
 | 
			
		||||
version = "1.1.0"
 | 
			
		||||
@ -3549,6 +3607,18 @@ dependencies = [
 | 
			
		||||
 "serde_derive",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde-wasm-bindgen"
 | 
			
		||||
version = "0.3.1"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "618365e8e586c22123d692b72a7d791d5ee697817b65a218cdf12a98870af0f7"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "fnv",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "serde_bytes"
 | 
			
		||||
version = "0.11.9"
 | 
			
		||||
@ -4453,8 +4523,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "wasm-bindgen-macro",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -4700,26 +4768,17 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "yew"
 | 
			
		||||
version = "0.18.0"
 | 
			
		||||
version = "0.19.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "e4d5154faef86dddd2eb333d4755ea5643787d20aca683e58759b0e53351409f"
 | 
			
		||||
checksum = "2a1ccb53e57d3f7d847338cf5758befa811cabe207df07f543c06f502f9998cd"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "anyhow",
 | 
			
		||||
 "anymap",
 | 
			
		||||
 "bincode",
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "cfg-match",
 | 
			
		||||
 "console_error_panic_hook",
 | 
			
		||||
 "gloo",
 | 
			
		||||
 "http",
 | 
			
		||||
 "gloo-utils",
 | 
			
		||||
 "indexmap",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "log",
 | 
			
		||||
 "ryu",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "scoped-tls-hkt",
 | 
			
		||||
 "slab",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "wasm-bindgen-futures",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
@ -4728,12 +4787,13 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "yew-macro"
 | 
			
		||||
version = "0.18.0"
 | 
			
		||||
version = "0.19.3"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "d6e23bfe3dc3933fbe9592d149c9985f3047d08c637a884b9344c21e56e092ef"
 | 
			
		||||
checksum = "5fab79082b556d768d6e21811869c761893f0450e1d550a67892b9bce303b7bb"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "boolinator",
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
 "proc-macro-error",
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
@ -4741,60 +4801,52 @@ dependencies = [
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "yew-router"
 | 
			
		||||
version = "0.15.0"
 | 
			
		||||
version = "0.16.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "27666236d9597eac9be560e841e415e20ba67020bc8cd081076be178e159c8bc"
 | 
			
		||||
checksum = "155804f6f3aa309f596d5c3fa14486a94e7756f1edd7634569949e401d5099f2"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "cfg-match",
 | 
			
		||||
 "gloo",
 | 
			
		||||
 "gloo-utils",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "log",
 | 
			
		||||
 "nom 5.1.2",
 | 
			
		||||
 "route-recognizer",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "serde-wasm-bindgen",
 | 
			
		||||
 "serde_urlencoded",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
 "yew",
 | 
			
		||||
 "yew-router-macro",
 | 
			
		||||
 "yew-router-route-parser",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "yew-router-macro"
 | 
			
		||||
version = "0.15.0"
 | 
			
		||||
version = "0.16.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "4c0ace2924b7a175e2d1c0e62ee7022a5ad840040dcd52414ce5f410ab322dba"
 | 
			
		||||
checksum = "39049d193b52eaad4ffc80916bf08806d142c90b5edcebd527644de438a7e19a"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
 "yew-router-route-parser",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "yew-router-route-parser"
 | 
			
		||||
version = "0.15.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "de4a67208fb46b900af18a7397938b01f379dfc18da34799cfa8347eec715697"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "nom 5.1.2",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "yew_form"
 | 
			
		||||
version = "0.1.8"
 | 
			
		||||
source = "git+https://github.com/jfbilodeau/yew_form?rev=67050812695b7a8a90b81b0637e347fc6629daed#67050812695b7a8a90b81b0637e347fc6629daed"
 | 
			
		||||
source = "git+https://github.com/jfbilodeau/yew_form?rev=4b9fabffb63393ec7626a4477fd36de12a07fac9#4b9fabffb63393ec7626a4477fd36de12a07fac9"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "gloo-console",
 | 
			
		||||
 "validator",
 | 
			
		||||
 "validator_derive 0.14.0",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
 "yew",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "yew_form_derive"
 | 
			
		||||
version = "0.1.8"
 | 
			
		||||
source = "git+https://github.com/jfbilodeau/yew_form?rev=67050812695b7a8a90b81b0637e347fc6629daed#67050812695b7a8a90b81b0637e347fc6629daed"
 | 
			
		||||
source = "git+https://github.com/jfbilodeau/yew_form?rev=4b9fabffb63393ec7626a4477fd36de12a07fac9#4b9fabffb63393ec7626a4477fd36de12a07fac9"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
@ -4802,19 +4854,6 @@ dependencies = [
 | 
			
		||||
 "yew_form",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "yewtil"
 | 
			
		||||
version = "0.4.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8543663ac49cd613df079282a1d8bdbdebdad6e02bac229f870fd4237b5d9aaa"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "log",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
 "wasm-bindgen-futures",
 | 
			
		||||
 "web-sys",
 | 
			
		||||
 "yew",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "zeroize"
 | 
			
		||||
version = "1.5.7"
 | 
			
		||||
 | 
			
		||||
@ -8,22 +8,25 @@ include = ["src/**/*", "queries/**/*", "Cargo.toml", "../schema.graphql"]
 | 
			
		||||
[dependencies]
 | 
			
		||||
anyhow = "1"
 | 
			
		||||
base64 = "0.13"
 | 
			
		||||
gloo-console = "0.2.3"
 | 
			
		||||
gloo-file = "0.2.3"
 | 
			
		||||
gloo-net = "*"
 | 
			
		||||
graphql_client = "0.10"
 | 
			
		||||
http = "0.2"
 | 
			
		||||
jwt = "0.13"
 | 
			
		||||
rand = "0.8"
 | 
			
		||||
serde = "1"
 | 
			
		||||
serde_json = "1"
 | 
			
		||||
url-escape = "0.1.1"
 | 
			
		||||
validator = "=0.14"
 | 
			
		||||
validator_derive = "*"
 | 
			
		||||
wasm-bindgen = "0.2"
 | 
			
		||||
yew = "0.18"
 | 
			
		||||
yewtil = "*"
 | 
			
		||||
yew-router = "0.15"
 | 
			
		||||
wasm-bindgen-futures = "*"
 | 
			
		||||
yew = "0.19.3"
 | 
			
		||||
yew-router = "0.16"
 | 
			
		||||
 | 
			
		||||
# Needed because of https://github.com/tkaitchuck/aHash/issues/95
 | 
			
		||||
indexmap = "=1.6.2"
 | 
			
		||||
url-escape = "0.1.1"
 | 
			
		||||
 | 
			
		||||
[dependencies.web-sys]
 | 
			
		||||
version = "0.3"
 | 
			
		||||
@ -56,11 +59,11 @@ version = "0.24"
 | 
			
		||||
 | 
			
		||||
[dependencies.yew_form]
 | 
			
		||||
git = "https://github.com/jfbilodeau/yew_form"
 | 
			
		||||
rev = "67050812695b7a8a90b81b0637e347fc6629daed"
 | 
			
		||||
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
 | 
			
		||||
 | 
			
		||||
[dependencies.yew_form_derive]
 | 
			
		||||
git = "https://github.com/jfbilodeau/yew_form"
 | 
			
		||||
rev = "67050812695b7a8a90b81b0637e347fc6629daed"
 | 
			
		||||
rev = "4b9fabffb63393ec7626a4477fd36de12a07fac9"
 | 
			
		||||
 | 
			
		||||
[lib]
 | 
			
		||||
crate-type = ["cdylib"]
 | 
			
		||||
 | 
			
		||||
@ -52,23 +52,25 @@ pub struct Props {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<AddGroupMemberComponent> for AddGroupMemberComponent {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::UserListResponse(response) => {
 | 
			
		||||
                self.user_list = Some(response?.users);
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::SubmitAddMember => return self.submit_add_member(),
 | 
			
		||||
            Msg::SubmitAddMember => return self.submit_add_member(ctx),
 | 
			
		||||
            Msg::AddMemberResponse(response) => {
 | 
			
		||||
                response?;
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                let user = self
 | 
			
		||||
                    .selected_user
 | 
			
		||||
                    .as_ref()
 | 
			
		||||
                    .expect("Could not get selected user")
 | 
			
		||||
                    .clone();
 | 
			
		||||
                // Remove the user from the dropdown.
 | 
			
		||||
                self.common.on_user_added_to_group.emit(user);
 | 
			
		||||
                ctx.props().on_user_added_to_group.emit(user);
 | 
			
		||||
            }
 | 
			
		||||
            Msg::SelectionChanged(option_props) => {
 | 
			
		||||
                let was_some = self.selected_user.is_some();
 | 
			
		||||
@ -88,23 +90,25 @@ impl CommonComponent<AddGroupMemberComponent> for AddGroupMemberComponent {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AddGroupMemberComponent {
 | 
			
		||||
    fn get_user_list(&mut self) {
 | 
			
		||||
    fn get_user_list(&mut self, ctx: &Context<Self>) {
 | 
			
		||||
        self.common.call_graphql::<ListUserNames, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            list_user_names::Variables { filters: None },
 | 
			
		||||
            Msg::UserListResponse,
 | 
			
		||||
            "Error trying to fetch user list",
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn submit_add_member(&mut self) -> Result<bool> {
 | 
			
		||||
    fn submit_add_member(&mut self, ctx: &Context<Self>) -> Result<bool> {
 | 
			
		||||
        let user_id = match self.selected_user.clone() {
 | 
			
		||||
            None => return Ok(false),
 | 
			
		||||
            Some(user) => user.id,
 | 
			
		||||
        };
 | 
			
		||||
        self.common.call_graphql::<AddUserToGroup, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            add_user_to_group::Variables {
 | 
			
		||||
                user: user_id,
 | 
			
		||||
                group: self.common.group_id,
 | 
			
		||||
                group: ctx.props().group_id,
 | 
			
		||||
            },
 | 
			
		||||
            Msg::AddMemberResponse,
 | 
			
		||||
            "Error trying to initiate adding the user to a group",
 | 
			
		||||
@ -112,8 +116,8 @@ impl AddGroupMemberComponent {
 | 
			
		||||
        Ok(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_selectable_user_list(&self, user_list: &[User]) -> Vec<User> {
 | 
			
		||||
        let user_groups = self.common.users.iter().collect::<HashSet<_>>();
 | 
			
		||||
    fn get_selectable_user_list(&self, ctx: &Context<Self>, user_list: &[User]) -> Vec<User> {
 | 
			
		||||
        let user_groups = ctx.props().users.iter().collect::<HashSet<_>>();
 | 
			
		||||
        user_list
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|u| !user_groups.contains(u))
 | 
			
		||||
@ -126,41 +130,39 @@ impl Component for AddGroupMemberComponent {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let mut res = Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            user_list: None,
 | 
			
		||||
            selected_user: None,
 | 
			
		||||
        };
 | 
			
		||||
        res.get_user_list();
 | 
			
		||||
        res.get_user_list(ctx);
 | 
			
		||||
        res
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update_and_report_error(
 | 
			
		||||
            self,
 | 
			
		||||
            ctx,
 | 
			
		||||
            msg,
 | 
			
		||||
            self.common.on_error.clone(),
 | 
			
		||||
            ctx.props().on_error.clone(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = ctx.link();
 | 
			
		||||
        if let Some(user_list) = &self.user_list {
 | 
			
		||||
            let to_add_user_list = self.get_selectable_user_list(user_list);
 | 
			
		||||
            let to_add_user_list = self.get_selectable_user_list(ctx, user_list);
 | 
			
		||||
            #[allow(unused_braces)]
 | 
			
		||||
            let make_select_option = |user: User| {
 | 
			
		||||
                html_nested! {
 | 
			
		||||
                    <SelectOption value=user.id.clone() text=user.display_name.clone() key=user.id />
 | 
			
		||||
                    <SelectOption value={user.id.clone()} text={user.display_name.clone()} key={user.id} />
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            html! {
 | 
			
		||||
            <div class="row">
 | 
			
		||||
              <div class="col-sm-3">
 | 
			
		||||
                <Select on_selection_change=self.common.callback(Msg::SelectionChanged)>
 | 
			
		||||
                <Select on_selection_change={link.callback(Msg::SelectionChanged)}>
 | 
			
		||||
                  {
 | 
			
		||||
                    to_add_user_list
 | 
			
		||||
                        .into_iter()
 | 
			
		||||
@ -172,8 +174,8 @@ impl Component for AddGroupMemberComponent {
 | 
			
		||||
              <div class="col-3">
 | 
			
		||||
                <button
 | 
			
		||||
                  class="btn btn-secondary"
 | 
			
		||||
                  disabled=self.selected_user.is_none() || self.common.is_task_running()
 | 
			
		||||
                  onclick=self.common.callback(|_| Msg::SubmitAddMember)>
 | 
			
		||||
                  disabled={self.selected_user.is_none() || self.common.is_task_running()}
 | 
			
		||||
                  onclick={link.callback(|_| Msg::SubmitAddMember)}>
 | 
			
		||||
                   <i class="bi-person-plus me-2"></i>
 | 
			
		||||
                  {"Add to group"}
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
@ -64,16 +64,18 @@ pub struct Props {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::GroupListResponse(response) => {
 | 
			
		||||
                self.group_list = Some(response?.groups.into_iter().map(Into::into).collect());
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::SubmitAddGroup => return self.submit_add_group(),
 | 
			
		||||
            Msg::SubmitAddGroup => return self.submit_add_group(ctx),
 | 
			
		||||
            Msg::AddGroupResponse(response) => {
 | 
			
		||||
                response?;
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                // Adding the user to the group succeeded, we're not in the process of adding a
 | 
			
		||||
                // group anymore.
 | 
			
		||||
                let group = self
 | 
			
		||||
@ -82,7 +84,7 @@ impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent {
 | 
			
		||||
                    .expect("Could not get selected group")
 | 
			
		||||
                    .clone();
 | 
			
		||||
                // Remove the group from the dropdown.
 | 
			
		||||
                self.common.on_user_added_to_group.emit(group);
 | 
			
		||||
                ctx.props().on_user_added_to_group.emit(group);
 | 
			
		||||
            }
 | 
			
		||||
            Msg::SelectionChanged(option_props) => {
 | 
			
		||||
                let was_some = self.selected_group.is_some();
 | 
			
		||||
@ -102,22 +104,24 @@ impl CommonComponent<AddUserToGroupComponent> for AddUserToGroupComponent {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AddUserToGroupComponent {
 | 
			
		||||
    fn get_group_list(&mut self) {
 | 
			
		||||
    fn get_group_list(&mut self, ctx: &Context<Self>) {
 | 
			
		||||
        self.common.call_graphql::<GetGroupList, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            get_group_list::Variables,
 | 
			
		||||
            Msg::GroupListResponse,
 | 
			
		||||
            "Error trying to fetch group list",
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn submit_add_group(&mut self) -> Result<bool> {
 | 
			
		||||
    fn submit_add_group(&mut self, ctx: &Context<Self>) -> Result<bool> {
 | 
			
		||||
        let group_id = match &self.selected_group {
 | 
			
		||||
            None => return Ok(false),
 | 
			
		||||
            Some(group) => group.id,
 | 
			
		||||
        };
 | 
			
		||||
        self.common.call_graphql::<AddUserToGroup, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            add_user_to_group::Variables {
 | 
			
		||||
                user: self.common.username.clone(),
 | 
			
		||||
                user: ctx.props().username.clone(),
 | 
			
		||||
                group: group_id,
 | 
			
		||||
            },
 | 
			
		||||
            Msg::AddGroupResponse,
 | 
			
		||||
@ -126,8 +130,8 @@ impl AddUserToGroupComponent {
 | 
			
		||||
        Ok(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn get_selectable_group_list(&self, group_list: &[Group]) -> Vec<Group> {
 | 
			
		||||
        let user_groups = self.common.groups.iter().collect::<HashSet<_>>();
 | 
			
		||||
    fn get_selectable_group_list(&self, props: &Props, group_list: &[Group]) -> Vec<Group> {
 | 
			
		||||
        let user_groups = props.groups.iter().collect::<HashSet<_>>();
 | 
			
		||||
        group_list
 | 
			
		||||
            .iter()
 | 
			
		||||
            .filter(|g| !user_groups.contains(g))
 | 
			
		||||
@ -139,41 +143,39 @@ impl AddUserToGroupComponent {
 | 
			
		||||
impl Component for AddUserToGroupComponent {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let mut res = Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            group_list: None,
 | 
			
		||||
            selected_group: None,
 | 
			
		||||
        };
 | 
			
		||||
        res.get_group_list();
 | 
			
		||||
        res.get_group_list(ctx);
 | 
			
		||||
        res
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update_and_report_error(
 | 
			
		||||
            self,
 | 
			
		||||
            ctx,
 | 
			
		||||
            msg,
 | 
			
		||||
            self.common.on_error.clone(),
 | 
			
		||||
            ctx.props().on_error.clone(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = ctx.link();
 | 
			
		||||
        if let Some(group_list) = &self.group_list {
 | 
			
		||||
            let to_add_group_list = self.get_selectable_group_list(group_list);
 | 
			
		||||
            let to_add_group_list = self.get_selectable_group_list(ctx.props(), group_list);
 | 
			
		||||
            #[allow(unused_braces)]
 | 
			
		||||
            let make_select_option = |group: Group| {
 | 
			
		||||
                html_nested! {
 | 
			
		||||
                    <SelectOption value=group.id.to_string() text=group.display_name key=group.id />
 | 
			
		||||
                    <SelectOption value={group.id.to_string()} text={group.display_name} key={group.id} />
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            html! {
 | 
			
		||||
            <div class="row">
 | 
			
		||||
              <div class="col-sm-3">
 | 
			
		||||
                <Select on_selection_change=self.common.callback(Msg::SelectionChanged)>
 | 
			
		||||
                <Select on_selection_change={link.callback(Msg::SelectionChanged)}>
 | 
			
		||||
                  {
 | 
			
		||||
                    to_add_group_list
 | 
			
		||||
                        .into_iter()
 | 
			
		||||
@ -185,8 +187,8 @@ impl Component for AddUserToGroupComponent {
 | 
			
		||||
              <div class="col-sm-3">
 | 
			
		||||
                <button
 | 
			
		||||
                  class="btn btn-secondary"
 | 
			
		||||
                  disabled=self.selected_group.is_none() || self.common.is_task_running()
 | 
			
		||||
                  onclick=self.common.callback(|_| Msg::SubmitAddGroup)>
 | 
			
		||||
                  disabled={self.selected_group.is_none() || self.common.is_task_running()}
 | 
			
		||||
                  onclick={link.callback(|_| Msg::SubmitAddGroup)}>
 | 
			
		||||
                  <i class="bi-person-plus me-2"></i>
 | 
			
		||||
                  {"Add to group"}
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
@ -9,31 +9,39 @@ use crate::{
 | 
			
		||||
        logout::LogoutButton,
 | 
			
		||||
        reset_password_step1::ResetPasswordStep1Form,
 | 
			
		||||
        reset_password_step2::ResetPasswordStep2Form,
 | 
			
		||||
        router::{AppRoute, Link, NavButton},
 | 
			
		||||
        router::{AppRoute, Link, Redirect},
 | 
			
		||||
        user_details::UserDetails,
 | 
			
		||||
        user_table::UserTable,
 | 
			
		||||
    },
 | 
			
		||||
    infra::{api::HostService, cookies::get_cookie},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use gloo_console::error;
 | 
			
		||||
use yew::{
 | 
			
		||||
    prelude::*,
 | 
			
		||||
    services::{fetch::FetchTask, ConsoleService},
 | 
			
		||||
    function_component,
 | 
			
		||||
    html::Scope,
 | 
			
		||||
    prelude::{html, Component, Html},
 | 
			
		||||
    Context,
 | 
			
		||||
};
 | 
			
		||||
use yew_router::{
 | 
			
		||||
    agent::{RouteAgentDispatcher, RouteRequest},
 | 
			
		||||
    route::Route,
 | 
			
		||||
    router::Router,
 | 
			
		||||
    service::RouteService,
 | 
			
		||||
    prelude::{History, Location},
 | 
			
		||||
    scope_ext::RouterScopeExt,
 | 
			
		||||
    BrowserRouter, Switch,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[function_component(AppContainer)]
 | 
			
		||||
pub fn app_container() -> Html {
 | 
			
		||||
    html! {
 | 
			
		||||
        <BrowserRouter>
 | 
			
		||||
            <App />
 | 
			
		||||
        </BrowserRouter>
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct App {
 | 
			
		||||
    link: ComponentLink<Self>,
 | 
			
		||||
    user_info: Option<(String, bool)>,
 | 
			
		||||
    redirect_to: Option<AppRoute>,
 | 
			
		||||
    route_dispatcher: RouteAgentDispatcher,
 | 
			
		||||
    password_reset_enabled: Option<bool>,
 | 
			
		||||
    task: Option<FetchTask>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum Msg {
 | 
			
		||||
@ -46,66 +54,57 @@ impl Component for App {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = ();
 | 
			
		||||
 | 
			
		||||
    fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
        let mut app = Self {
 | 
			
		||||
            link,
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let app = Self {
 | 
			
		||||
            user_info: get_cookie("user_id")
 | 
			
		||||
                .unwrap_or_else(|e| {
 | 
			
		||||
                    ConsoleService::error(&e.to_string());
 | 
			
		||||
                    error!(&e.to_string());
 | 
			
		||||
                    None
 | 
			
		||||
                })
 | 
			
		||||
                .and_then(|u| {
 | 
			
		||||
                    get_cookie("is_admin")
 | 
			
		||||
                        .map(|so| so.map(|s| (u, s == "true")))
 | 
			
		||||
                        .unwrap_or_else(|e| {
 | 
			
		||||
                            ConsoleService::error(&e.to_string());
 | 
			
		||||
                            error!(&e.to_string());
 | 
			
		||||
                            None
 | 
			
		||||
                        })
 | 
			
		||||
                }),
 | 
			
		||||
            redirect_to: Self::get_redirect_route(),
 | 
			
		||||
            route_dispatcher: RouteAgentDispatcher::new(),
 | 
			
		||||
            redirect_to: Self::get_redirect_route(ctx),
 | 
			
		||||
            password_reset_enabled: None,
 | 
			
		||||
            task: None,
 | 
			
		||||
        };
 | 
			
		||||
        app.task = Some(
 | 
			
		||||
            HostService::probe_password_reset(
 | 
			
		||||
                app.link.callback_once(Msg::PasswordResetProbeFinished),
 | 
			
		||||
            )
 | 
			
		||||
            .unwrap(),
 | 
			
		||||
        );
 | 
			
		||||
        app.apply_initial_redirections();
 | 
			
		||||
        ctx.link().send_future(async move {
 | 
			
		||||
            Msg::PasswordResetProbeFinished(HostService::probe_password_reset().await)
 | 
			
		||||
        });
 | 
			
		||||
        app.apply_initial_redirections(ctx);
 | 
			
		||||
        app
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        let history = ctx.link().history().unwrap();
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::Login((user_name, is_admin)) => {
 | 
			
		||||
                self.user_info = Some((user_name.clone(), is_admin));
 | 
			
		||||
                self.route_dispatcher
 | 
			
		||||
                    .send(RouteRequest::ChangeRoute(Route::from(
 | 
			
		||||
                        self.redirect_to.take().unwrap_or_else(|| {
 | 
			
		||||
                history.push(self.redirect_to.take().unwrap_or_else(|| {
 | 
			
		||||
                    if is_admin {
 | 
			
		||||
                        AppRoute::ListUsers
 | 
			
		||||
                    } else {
 | 
			
		||||
                                AppRoute::UserDetails(user_name.clone())
 | 
			
		||||
                        AppRoute::UserDetails {
 | 
			
		||||
                            user_id: user_name.clone(),
 | 
			
		||||
                        }
 | 
			
		||||
                        }),
 | 
			
		||||
                    )));
 | 
			
		||||
                    }
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
            Msg::Logout => {
 | 
			
		||||
                self.user_info = None;
 | 
			
		||||
                self.redirect_to = None;
 | 
			
		||||
                self.route_dispatcher
 | 
			
		||||
                    .send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login)));
 | 
			
		||||
                history.push(AppRoute::Login);
 | 
			
		||||
            }
 | 
			
		||||
            Msg::PasswordResetProbeFinished(Ok(enabled)) => {
 | 
			
		||||
                self.task = None;
 | 
			
		||||
                self.password_reset_enabled = Some(enabled);
 | 
			
		||||
            }
 | 
			
		||||
            Msg::PasswordResetProbeFinished(Err(err)) => {
 | 
			
		||||
                self.task = None;
 | 
			
		||||
                self.password_reset_enabled = Some(false);
 | 
			
		||||
                ConsoleService::error(&format!(
 | 
			
		||||
                error!(&format!(
 | 
			
		||||
                    "Could not probe for password reset support: {err:#}"
 | 
			
		||||
                ));
 | 
			
		||||
            }
 | 
			
		||||
@ -113,24 +112,20 @@ impl Component for App {
 | 
			
		||||
        true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, _: Self::Properties) -> ShouldRender {
 | 
			
		||||
        false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
        let link = self.link.clone();
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = ctx.link().clone();
 | 
			
		||||
        let is_admin = self.is_admin();
 | 
			
		||||
        let password_reset_enabled = self.password_reset_enabled;
 | 
			
		||||
        html! {
 | 
			
		||||
          <div>
 | 
			
		||||
            {self.view_banner()}
 | 
			
		||||
            {self.view_banner(ctx)}
 | 
			
		||||
            <div class="container py-3 bg-kug">
 | 
			
		||||
              <div class="row justify-content-center" style="padding-bottom: 80px;">
 | 
			
		||||
                <div class="py-3" style="max-width: 1000px">
 | 
			
		||||
                  <Router<AppRoute>
 | 
			
		||||
                    render = Router::render(move |s| Self::dispatch_route(s, &link, is_admin, password_reset_enabled))
 | 
			
		||||
                <main class="py-3" style="max-width: 1000px">
 | 
			
		||||
                  <Switch<AppRoute>
 | 
			
		||||
                    render={Switch::render(move |routes| Self::dispatch_route(routes, &link, is_admin, password_reset_enabled))}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                </main>
 | 
			
		||||
              </div>
 | 
			
		||||
              {self.view_footer()}
 | 
			
		||||
            </div>
 | 
			
		||||
@ -140,65 +135,56 @@ impl Component for App {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl App {
 | 
			
		||||
    fn get_redirect_route() -> Option<AppRoute> {
 | 
			
		||||
        let route_service = RouteService::<()>::new();
 | 
			
		||||
        let current_route = route_service.get_path();
 | 
			
		||||
        if current_route.is_empty()
 | 
			
		||||
            || current_route == "/"
 | 
			
		||||
            || current_route.contains("login")
 | 
			
		||||
            || current_route.contains("reset-password")
 | 
			
		||||
        {
 | 
			
		||||
            None
 | 
			
		||||
        } else {
 | 
			
		||||
            use yew_router::Switch;
 | 
			
		||||
            AppRoute::from_route_part::<()>(current_route, None).0
 | 
			
		||||
        }
 | 
			
		||||
    // Get the page to land on after logging in, defaulting to the index.
 | 
			
		||||
    fn get_redirect_route(ctx: &Context<Self>) -> Option<AppRoute> {
 | 
			
		||||
        let route = ctx.link().history().unwrap().location().route::<AppRoute>();
 | 
			
		||||
        route.filter(|route| {
 | 
			
		||||
            !matches!(
 | 
			
		||||
                route,
 | 
			
		||||
                AppRoute::Index
 | 
			
		||||
                    | AppRoute::Login
 | 
			
		||||
                    | AppRoute::StartResetPassword
 | 
			
		||||
                    | AppRoute::FinishResetPassword { token: _ }
 | 
			
		||||
            )
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn apply_initial_redirections(&mut self) {
 | 
			
		||||
        let route_service = RouteService::<()>::new();
 | 
			
		||||
        let current_route = route_service.get_path();
 | 
			
		||||
        if current_route.contains("reset-password") {
 | 
			
		||||
            if self.password_reset_enabled == Some(false) {
 | 
			
		||||
                self.route_dispatcher
 | 
			
		||||
                    .send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login)));
 | 
			
		||||
            }
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        match &self.user_info {
 | 
			
		||||
            None => {
 | 
			
		||||
                self.route_dispatcher
 | 
			
		||||
                    .send(RouteRequest::ReplaceRoute(Route::from(AppRoute::Login)));
 | 
			
		||||
            }
 | 
			
		||||
            Some((user_name, is_admin)) => match &self.redirect_to {
 | 
			
		||||
                Some(url) => {
 | 
			
		||||
                    self.route_dispatcher
 | 
			
		||||
                        .send(RouteRequest::ReplaceRoute(Route::from(url.clone())));
 | 
			
		||||
                }
 | 
			
		||||
                None => {
 | 
			
		||||
    fn apply_initial_redirections(&self, ctx: &Context<Self>) {
 | 
			
		||||
        let history = ctx.link().history().unwrap();
 | 
			
		||||
        let route = history.location().route::<AppRoute>();
 | 
			
		||||
        let redirection = match (route, &self.user_info, &self.redirect_to) {
 | 
			
		||||
            (
 | 
			
		||||
                Some(AppRoute::StartResetPassword | AppRoute::FinishResetPassword { token: _ }),
 | 
			
		||||
                _,
 | 
			
		||||
                _,
 | 
			
		||||
            ) if self.password_reset_enabled == Some(false) => Some(AppRoute::Login),
 | 
			
		||||
            (None, _, _) | (_, None, _) => Some(AppRoute::Login),
 | 
			
		||||
            // User is logged in, a URL was given, don't redirect.
 | 
			
		||||
            (_, Some(_), Some(_)) => None,
 | 
			
		||||
            (_, Some((user_name, is_admin)), None) => {
 | 
			
		||||
                if *is_admin {
 | 
			
		||||
                        self.route_dispatcher
 | 
			
		||||
                            .send(RouteRequest::ReplaceRoute(Route::from(AppRoute::ListUsers)));
 | 
			
		||||
                    Some(AppRoute::ListUsers)
 | 
			
		||||
                } else {
 | 
			
		||||
                        self.route_dispatcher
 | 
			
		||||
                            .send(RouteRequest::ReplaceRoute(Route::from(
 | 
			
		||||
                                AppRoute::UserDetails(user_name.clone()),
 | 
			
		||||
                            )));
 | 
			
		||||
                    Some(AppRoute::UserDetails {
 | 
			
		||||
                        user_id: user_name.clone(),
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
        if let Some(redirect_to) = redirection {
 | 
			
		||||
            history.push(redirect_to);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn dispatch_route(
 | 
			
		||||
        switch: AppRoute,
 | 
			
		||||
        link: &ComponentLink<Self>,
 | 
			
		||||
        switch: &AppRoute,
 | 
			
		||||
        link: &Scope<Self>,
 | 
			
		||||
        is_admin: bool,
 | 
			
		||||
        password_reset_enabled: Option<bool>,
 | 
			
		||||
    ) -> Html {
 | 
			
		||||
        match switch {
 | 
			
		||||
            AppRoute::Login => html! {
 | 
			
		||||
                <LoginForm on_logged_in=link.callback(Msg::Login) password_reset_enabled=password_reset_enabled.unwrap_or(false)/>
 | 
			
		||||
                <LoginForm on_logged_in={link.callback(Msg::Login)} password_reset_enabled={password_reset_enabled.unwrap_or(false)}/>
 | 
			
		||||
            },
 | 
			
		||||
            AppRoute::CreateUser => html! {
 | 
			
		||||
                <CreateUserForm/>
 | 
			
		||||
@ -206,10 +192,10 @@ impl App {
 | 
			
		||||
            AppRoute::Index | AppRoute::ListUsers => html! {
 | 
			
		||||
                <div>
 | 
			
		||||
                  <UserTable />
 | 
			
		||||
                  <NavButton classes="btn btn-primary" route=AppRoute::CreateUser>
 | 
			
		||||
                  <Link classes="btn btn-primary" to={AppRoute::CreateUser}>
 | 
			
		||||
                    <i class="bi-person-plus me-2"></i>
 | 
			
		||||
                    {"Create a user"}
 | 
			
		||||
                  </NavButton>
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </div>
 | 
			
		||||
            },
 | 
			
		||||
            AppRoute::CreateGroup => html! {
 | 
			
		||||
@ -218,40 +204,40 @@ impl App {
 | 
			
		||||
            AppRoute::ListGroups => html! {
 | 
			
		||||
                <div>
 | 
			
		||||
                  <GroupTable />
 | 
			
		||||
                  <NavButton classes="btn btn-primary" route=AppRoute::CreateGroup>
 | 
			
		||||
                  <Link classes="btn btn-primary" to={AppRoute::CreateGroup}>
 | 
			
		||||
                    <i class="bi-plus-circle me-2"></i>
 | 
			
		||||
                    {"Create a group"}
 | 
			
		||||
                  </NavButton>
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </div>
 | 
			
		||||
            },
 | 
			
		||||
            AppRoute::GroupDetails(group_id) => html! {
 | 
			
		||||
                <GroupDetails group_id=group_id />
 | 
			
		||||
            AppRoute::GroupDetails { group_id } => html! {
 | 
			
		||||
                <GroupDetails group_id={*group_id} />
 | 
			
		||||
            },
 | 
			
		||||
            AppRoute::UserDetails(username) => html! {
 | 
			
		||||
                <UserDetails username=username is_admin=is_admin />
 | 
			
		||||
            AppRoute::UserDetails { user_id } => html! {
 | 
			
		||||
                <UserDetails username={user_id.clone()} is_admin={is_admin} />
 | 
			
		||||
            },
 | 
			
		||||
            AppRoute::ChangePassword(username) => html! {
 | 
			
		||||
                <ChangePasswordForm username=username is_admin=is_admin />
 | 
			
		||||
            AppRoute::ChangePassword { user_id } => html! {
 | 
			
		||||
                <ChangePasswordForm username={user_id.clone()} is_admin={is_admin} />
 | 
			
		||||
            },
 | 
			
		||||
            AppRoute::StartResetPassword => match password_reset_enabled {
 | 
			
		||||
                Some(true) => html! { <ResetPasswordStep1Form /> },
 | 
			
		||||
                Some(false) => {
 | 
			
		||||
                    App::dispatch_route(AppRoute::Login, link, is_admin, password_reset_enabled)
 | 
			
		||||
                    html! { <Redirect to={AppRoute::Login}/> }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                None => html! {},
 | 
			
		||||
            },
 | 
			
		||||
            AppRoute::FinishResetPassword(token) => match password_reset_enabled {
 | 
			
		||||
                Some(true) => html! { <ResetPasswordStep2Form token=token /> },
 | 
			
		||||
            AppRoute::FinishResetPassword { token } => match password_reset_enabled {
 | 
			
		||||
                Some(true) => html! { <ResetPasswordStep2Form token={token.clone()} /> },
 | 
			
		||||
                Some(false) => {
 | 
			
		||||
                    App::dispatch_route(AppRoute::Login, link, is_admin, password_reset_enabled)
 | 
			
		||||
                    html! { <Redirect to={AppRoute::Login}/> }
 | 
			
		||||
                }
 | 
			
		||||
                None => html! {},
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_banner(&self) -> Html {
 | 
			
		||||
    fn view_banner(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
          <header class="p-2 mb-3 border-bottom">
 | 
			
		||||
            <div class="container">
 | 
			
		||||
@ -266,7 +252,7 @@ impl App {
 | 
			
		||||
                      <li>
 | 
			
		||||
                        <Link
 | 
			
		||||
                          classes="nav-link px-2 link-dark h6"
 | 
			
		||||
                          route=AppRoute::ListUsers>
 | 
			
		||||
                          to={AppRoute::ListUsers}>
 | 
			
		||||
                          <i class="bi-people me-2"></i>
 | 
			
		||||
                          {"Users"}
 | 
			
		||||
                        </Link>
 | 
			
		||||
@ -274,7 +260,7 @@ impl App {
 | 
			
		||||
                      <li>
 | 
			
		||||
                        <Link
 | 
			
		||||
                          classes="nav-link px-2 link-dark h6"
 | 
			
		||||
                          route=AppRoute::ListGroups>
 | 
			
		||||
                          to={AppRoute::ListGroups}>
 | 
			
		||||
                          <i class="bi-collection me-2"></i>
 | 
			
		||||
                          {"Groups"}
 | 
			
		||||
                        </Link>
 | 
			
		||||
@ -282,9 +268,16 @@ impl App {
 | 
			
		||||
                    </>
 | 
			
		||||
                  } } else { html!{} } }
 | 
			
		||||
                </ul>
 | 
			
		||||
                { self.view_user_menu(ctx) }
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </header>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
                {
 | 
			
		||||
    fn view_user_menu(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        if let Some((user_id, _)) = &self.user_info {
 | 
			
		||||
            let link = ctx.link();
 | 
			
		||||
            html! {
 | 
			
		||||
              <div class="dropdown text-end">
 | 
			
		||||
                <a href="#"
 | 
			
		||||
@ -312,22 +305,19 @@ impl App {
 | 
			
		||||
                  <li>
 | 
			
		||||
                    <Link
 | 
			
		||||
                      classes="dropdown-item"
 | 
			
		||||
                              route=AppRoute::UserDetails(user_id.clone())>
 | 
			
		||||
                      to={AppRoute::UserDetails{ user_id: user_id.clone() }}>
 | 
			
		||||
                      {"View details"}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li><hr class="dropdown-divider" /></li>
 | 
			
		||||
                  <li>
 | 
			
		||||
                            <LogoutButton on_logged_out=self.link.callback(|_| Msg::Logout) />
 | 
			
		||||
                    <LogoutButton on_logged_out={link.callback(|_| Msg::Logout)} />
 | 
			
		||||
                  </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
              </div>
 | 
			
		||||
            }
 | 
			
		||||
                  } else { html!{} }
 | 
			
		||||
                }
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </header>
 | 
			
		||||
        } else {
 | 
			
		||||
            html! {}
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,18 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    components::router::{AppRoute, NavButton},
 | 
			
		||||
    components::router::{AppRoute, Link},
 | 
			
		||||
    infra::{
 | 
			
		||||
        api::HostService,
 | 
			
		||||
        common_component::{CommonComponent, CommonComponentParts},
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{anyhow, bail, Context, Result};
 | 
			
		||||
use anyhow::{anyhow, bail, Result};
 | 
			
		||||
use gloo_console::error;
 | 
			
		||||
use lldap_auth::*;
 | 
			
		||||
use validator_derive::Validate;
 | 
			
		||||
use yew::{prelude::*, services::ConsoleService};
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
use yew_form::Form;
 | 
			
		||||
use yew_form_derive::Model;
 | 
			
		||||
use yew_router::{
 | 
			
		||||
    agent::{RouteAgentDispatcher, RouteRequest},
 | 
			
		||||
    route::Route,
 | 
			
		||||
};
 | 
			
		||||
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Default)]
 | 
			
		||||
enum OpaqueData {
 | 
			
		||||
@ -56,7 +54,6 @@ pub struct ChangePasswordForm {
 | 
			
		||||
    common: CommonComponentParts<Self>,
 | 
			
		||||
    form: Form<FormModel>,
 | 
			
		||||
    opaque_data: OpaqueData,
 | 
			
		||||
    route_dispatcher: RouteAgentDispatcher,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, PartialEq, Eq, Properties)]
 | 
			
		||||
@ -75,15 +72,20 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        use anyhow::Context;
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::FormUpdate => Ok(true),
 | 
			
		||||
            Msg::Submit => {
 | 
			
		||||
                if !self.form.validate() {
 | 
			
		||||
                    bail!("Check the form for errors");
 | 
			
		||||
                }
 | 
			
		||||
                if self.common.is_admin {
 | 
			
		||||
                    self.handle_msg(Msg::SubmitNewPassword)
 | 
			
		||||
                if ctx.props().is_admin {
 | 
			
		||||
                    self.handle_msg(ctx, Msg::SubmitNewPassword)
 | 
			
		||||
                } else {
 | 
			
		||||
                    let old_password = self.form.model().old_password;
 | 
			
		||||
                    if old_password.is_empty() {
 | 
			
		||||
@ -95,14 +97,14 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
 | 
			
		||||
                            .context("Could not initialize login")?;
 | 
			
		||||
                    self.opaque_data = OpaqueData::Login(login_start_request.state);
 | 
			
		||||
                    let req = login::ClientLoginStartRequest {
 | 
			
		||||
                        username: self.common.username.clone(),
 | 
			
		||||
                        username: ctx.props().username.clone(),
 | 
			
		||||
                        login_start_request: login_start_request.message,
 | 
			
		||||
                    };
 | 
			
		||||
                    self.common.call_backend(
 | 
			
		||||
                        HostService::login_start,
 | 
			
		||||
                        req,
 | 
			
		||||
                        ctx,
 | 
			
		||||
                        HostService::login_start(req),
 | 
			
		||||
                        Msg::AuthenticationStartResponse,
 | 
			
		||||
                    )?;
 | 
			
		||||
                    );
 | 
			
		||||
                    Ok(true)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@ -114,17 +116,14 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
 | 
			
		||||
                            |e| {
 | 
			
		||||
                                // Common error, we want to print a full error to the console but only a
 | 
			
		||||
                                // simple one to the user.
 | 
			
		||||
                                ConsoleService::error(&format!(
 | 
			
		||||
                                    "Invalid username or password: {}",
 | 
			
		||||
                                    e
 | 
			
		||||
                                ));
 | 
			
		||||
                                error!(&format!("Invalid username or password: {}", e));
 | 
			
		||||
                                anyhow!("Invalid username or password")
 | 
			
		||||
                            },
 | 
			
		||||
                        )?;
 | 
			
		||||
                    }
 | 
			
		||||
                    _ => panic!("Unexpected data in opaque_data field"),
 | 
			
		||||
                };
 | 
			
		||||
                self.handle_msg(Msg::SubmitNewPassword)
 | 
			
		||||
                self.handle_msg(ctx, Msg::SubmitNewPassword)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::SubmitNewPassword => {
 | 
			
		||||
                let mut rng = rand::rngs::OsRng;
 | 
			
		||||
@ -133,15 +132,15 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
 | 
			
		||||
                    opaque::client::registration::start_registration(&new_password, &mut rng)
 | 
			
		||||
                        .context("Could not initiate password change")?;
 | 
			
		||||
                let req = registration::ClientRegistrationStartRequest {
 | 
			
		||||
                    username: self.common.username.clone(),
 | 
			
		||||
                    username: ctx.props().username.clone(),
 | 
			
		||||
                    registration_start_request: registration_start_request.message,
 | 
			
		||||
                };
 | 
			
		||||
                self.opaque_data = OpaqueData::Registration(registration_start_request.state);
 | 
			
		||||
                self.common.call_backend(
 | 
			
		||||
                    HostService::register_start,
 | 
			
		||||
                    req,
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    HostService::register_start(req),
 | 
			
		||||
                    Msg::RegistrationStartResponse,
 | 
			
		||||
                )?;
 | 
			
		||||
                );
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::RegistrationStartResponse(res) => {
 | 
			
		||||
@ -161,22 +160,20 @@ impl CommonComponent<ChangePasswordForm> for ChangePasswordForm {
 | 
			
		||||
                            registration_upload: registration_finish.message,
 | 
			
		||||
                        };
 | 
			
		||||
                        self.common.call_backend(
 | 
			
		||||
                            HostService::register_finish,
 | 
			
		||||
                            req,
 | 
			
		||||
                            ctx,
 | 
			
		||||
                            HostService::register_finish(req),
 | 
			
		||||
                            Msg::RegistrationFinishResponse,
 | 
			
		||||
                        )
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                    _ => panic!("Unexpected data in opaque_data field"),
 | 
			
		||||
                }?;
 | 
			
		||||
                };
 | 
			
		||||
                Ok(false)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::RegistrationFinishResponse(response) => {
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                if response.is_ok() {
 | 
			
		||||
                    self.route_dispatcher
 | 
			
		||||
                        .send(RouteRequest::ChangeRoute(Route::from(
 | 
			
		||||
                            AppRoute::UserDetails(self.common.username.clone()),
 | 
			
		||||
                        )));
 | 
			
		||||
                    ctx.link().history().unwrap().push(AppRoute::UserDetails {
 | 
			
		||||
                        user_id: ctx.props().username.clone(),
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
                response?;
 | 
			
		||||
                Ok(true)
 | 
			
		||||
@ -193,25 +190,21 @@ impl Component for ChangePasswordForm {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(_: &Context<Self>) -> Self {
 | 
			
		||||
        ChangePasswordForm {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            form: yew_form::Form::<FormModel>::new(FormModel::default()),
 | 
			
		||||
            opaque_data: OpaqueData::None,
 | 
			
		||||
            route_dispatcher: RouteAgentDispatcher::new(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
        let is_admin = self.common.is_admin;
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let is_admin = ctx.props().is_admin;
 | 
			
		||||
        let link = ctx.link();
 | 
			
		||||
        type Field = yew_form::Field<FormModel>;
 | 
			
		||||
        html! {
 | 
			
		||||
          <>
 | 
			
		||||
@ -239,14 +232,14 @@ impl Component for ChangePasswordForm {
 | 
			
		||||
                  </label>
 | 
			
		||||
                  <div class="col-sm-10">
 | 
			
		||||
                    <Field
 | 
			
		||||
                      form=&self.form
 | 
			
		||||
                      form={&self.form}
 | 
			
		||||
                      field_name="old_password"
 | 
			
		||||
                      input_type="password"
 | 
			
		||||
                      class="form-control"
 | 
			
		||||
                      class_invalid="is-invalid has-error"
 | 
			
		||||
                      class_valid="has-success"
 | 
			
		||||
                      autocomplete="current-password"
 | 
			
		||||
                      oninput=self.common.callback(|_| Msg::FormUpdate) />
 | 
			
		||||
                      oninput={link.callback(|_| Msg::FormUpdate)} />
 | 
			
		||||
                    <div class="invalid-feedback">
 | 
			
		||||
                      {&self.form.field_message("old_password")}
 | 
			
		||||
                    </div>
 | 
			
		||||
@ -262,14 +255,14 @@ impl Component for ChangePasswordForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-sm-10">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="password"
 | 
			
		||||
                    input_type="password"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    autocomplete="new-password"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::FormUpdate) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::FormUpdate)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("password")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -284,14 +277,14 @@ impl Component for ChangePasswordForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-sm-10">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="confirm_password"
 | 
			
		||||
                    input_type="password"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    autocomplete="new-password"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::FormUpdate) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::FormUpdate)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("confirm_password")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -301,17 +294,17 @@ impl Component for ChangePasswordForm {
 | 
			
		||||
                <button
 | 
			
		||||
                  class="btn btn-primary col-auto col-form-label"
 | 
			
		||||
                  type="submit"
 | 
			
		||||
                  disabled=self.common.is_task_running()
 | 
			
		||||
                  onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
 | 
			
		||||
                  disabled={self.common.is_task_running()}
 | 
			
		||||
                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
 | 
			
		||||
                  <i class="bi-save me-2"></i>
 | 
			
		||||
                  {"Save changes"}
 | 
			
		||||
                </button>
 | 
			
		||||
                <NavButton
 | 
			
		||||
                <Link
 | 
			
		||||
                  classes="btn btn-secondary ms-2 col-auto col-form-label"
 | 
			
		||||
                  route=AppRoute::UserDetails(self.common.username.clone())>
 | 
			
		||||
                  to={AppRoute::UserDetails{user_id: ctx.props().username.clone()}}>
 | 
			
		||||
                  <i class="bi-arrow-return-left me-2"></i>
 | 
			
		||||
                  {"Back"}
 | 
			
		||||
                </NavButton>
 | 
			
		||||
                </Link>
 | 
			
		||||
              </div>
 | 
			
		||||
            </form>
 | 
			
		||||
          </>
 | 
			
		||||
 | 
			
		||||
@ -3,15 +3,12 @@ use crate::{
 | 
			
		||||
    infra::common_component::{CommonComponent, CommonComponentParts},
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{bail, Result};
 | 
			
		||||
use gloo_console::log;
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use validator_derive::Validate;
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
use yew::services::ConsoleService;
 | 
			
		||||
use yew_form_derive::Model;
 | 
			
		||||
use yew_router::{
 | 
			
		||||
    agent::{RouteAgentDispatcher, RouteRequest},
 | 
			
		||||
    route::Route,
 | 
			
		||||
};
 | 
			
		||||
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
 | 
			
		||||
 | 
			
		||||
#[derive(GraphQLQuery)]
 | 
			
		||||
#[graphql(
 | 
			
		||||
@ -24,7 +21,6 @@ pub struct CreateGroup;
 | 
			
		||||
 | 
			
		||||
pub struct CreateGroupForm {
 | 
			
		||||
    common: CommonComponentParts<Self>,
 | 
			
		||||
    route_dispatcher: RouteAgentDispatcher,
 | 
			
		||||
    form: yew_form::Form<CreateGroupModel>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -41,7 +37,11 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<CreateGroupForm> for CreateGroupForm {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::Update => Ok(true),
 | 
			
		||||
            Msg::SubmitForm => {
 | 
			
		||||
@ -53,6 +53,7 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
 | 
			
		||||
                    name: model.groupname,
 | 
			
		||||
                };
 | 
			
		||||
                self.common.call_graphql::<CreateGroup, _>(
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    req,
 | 
			
		||||
                    Msg::CreateGroupResponse,
 | 
			
		||||
                    "Error trying to create group",
 | 
			
		||||
@ -60,12 +61,11 @@ impl CommonComponent<CreateGroupForm> for CreateGroupForm {
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::CreateGroupResponse(response) => {
 | 
			
		||||
                ConsoleService::log(&format!(
 | 
			
		||||
                log!(&format!(
 | 
			
		||||
                    "Created group '{}'",
 | 
			
		||||
                    &response?.create_group.display_name
 | 
			
		||||
                ));
 | 
			
		||||
                self.route_dispatcher
 | 
			
		||||
                    .send(RouteRequest::ChangeRoute(Route::from(AppRoute::ListGroups)));
 | 
			
		||||
                ctx.link().history().unwrap().push(AppRoute::ListGroups);
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -80,23 +80,19 @@ impl Component for CreateGroupForm {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = ();
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(_: &Context<Self>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            route_dispatcher: RouteAgentDispatcher::new(),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            form: yew_form::Form::<CreateGroupModel>::new(CreateGroupModel::default()),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = ctx.link();
 | 
			
		||||
        type Field = yew_form::Field<CreateGroupModel>;
 | 
			
		||||
        html! {
 | 
			
		||||
          <div class="row justify-content-center">
 | 
			
		||||
@ -113,13 +109,13 @@ impl Component for CreateGroupForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="groupname"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    autocomplete="groupname"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("groupname")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -129,8 +125,8 @@ impl Component for CreateGroupForm {
 | 
			
		||||
                <button
 | 
			
		||||
                  class="btn btn-primary col-auto col-form-label"
 | 
			
		||||
                  type="submit"
 | 
			
		||||
                  disabled=self.common.is_task_running()
 | 
			
		||||
                  onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})>
 | 
			
		||||
                  disabled={self.common.is_task_running()}
 | 
			
		||||
                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}>
 | 
			
		||||
                  <i class="bi-save me-2"></i>
 | 
			
		||||
                  {"Submit"}
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
@ -5,17 +5,14 @@ use crate::{
 | 
			
		||||
        common_component::{CommonComponent, CommonComponentParts},
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{bail, Context, Result};
 | 
			
		||||
use anyhow::{bail, Result};
 | 
			
		||||
use gloo_console::log;
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use lldap_auth::{opaque, registration};
 | 
			
		||||
use validator_derive::Validate;
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
use yew::services::ConsoleService;
 | 
			
		||||
use yew_form_derive::Model;
 | 
			
		||||
use yew_router::{
 | 
			
		||||
    agent::{RouteAgentDispatcher, RouteRequest},
 | 
			
		||||
    route::Route,
 | 
			
		||||
};
 | 
			
		||||
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
 | 
			
		||||
 | 
			
		||||
#[derive(GraphQLQuery)]
 | 
			
		||||
#[graphql(
 | 
			
		||||
@ -28,7 +25,6 @@ pub struct CreateUser;
 | 
			
		||||
 | 
			
		||||
pub struct CreateUserForm {
 | 
			
		||||
    common: CommonComponentParts<Self>,
 | 
			
		||||
    route_dispatcher: RouteAgentDispatcher,
 | 
			
		||||
    form: yew_form::Form<CreateUserModel>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -73,7 +69,11 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<CreateUserForm> for CreateUserForm {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::Update => Ok(true),
 | 
			
		||||
            Msg::SubmitForm => {
 | 
			
		||||
@ -93,6 +93,7 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
 | 
			
		||||
                    },
 | 
			
		||||
                };
 | 
			
		||||
                self.common.call_graphql::<CreateUser, _>(
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    req,
 | 
			
		||||
                    Msg::CreateUserResponse,
 | 
			
		||||
                    "Error trying to create user",
 | 
			
		||||
@ -102,7 +103,7 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
 | 
			
		||||
            Msg::CreateUserResponse(r) => {
 | 
			
		||||
                match r {
 | 
			
		||||
                    Err(e) => return Err(e),
 | 
			
		||||
                    Ok(r) => ConsoleService::log(&format!(
 | 
			
		||||
                    Ok(r) => log!(&format!(
 | 
			
		||||
                        "Created user '{}' at '{}'",
 | 
			
		||||
                        &r.create_user.id, &r.create_user.creation_date
 | 
			
		||||
                    )),
 | 
			
		||||
@ -122,12 +123,11 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
 | 
			
		||||
                        registration_start_request: message,
 | 
			
		||||
                    };
 | 
			
		||||
                    self.common
 | 
			
		||||
                        .call_backend(HostService::register_start, req, move |r| {
 | 
			
		||||
                        .call_backend(ctx, HostService::register_start(req), move |r| {
 | 
			
		||||
                            Msg::RegistrationStartResponse((state, r))
 | 
			
		||||
                        })
 | 
			
		||||
                        .context("Error trying to create user")?;
 | 
			
		||||
                        });
 | 
			
		||||
                } else {
 | 
			
		||||
                    self.update(Msg::SuccessfulCreation);
 | 
			
		||||
                    self.update(ctx, Msg::SuccessfulCreation);
 | 
			
		||||
                }
 | 
			
		||||
                Ok(false)
 | 
			
		||||
            }
 | 
			
		||||
@ -143,22 +143,19 @@ impl CommonComponent<CreateUserForm> for CreateUserForm {
 | 
			
		||||
                    server_data: response.server_data,
 | 
			
		||||
                    registration_upload: registration_upload.message,
 | 
			
		||||
                };
 | 
			
		||||
                self.common
 | 
			
		||||
                    .call_backend(
 | 
			
		||||
                        HostService::register_finish,
 | 
			
		||||
                        req,
 | 
			
		||||
                self.common.call_backend(
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    HostService::register_finish(req),
 | 
			
		||||
                    Msg::RegistrationFinishResponse,
 | 
			
		||||
                    )
 | 
			
		||||
                    .context("Error trying to register user")?;
 | 
			
		||||
                );
 | 
			
		||||
                Ok(false)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::RegistrationFinishResponse(response) => {
 | 
			
		||||
                response?;
 | 
			
		||||
                self.handle_msg(Msg::SuccessfulCreation)
 | 
			
		||||
                self.handle_msg(ctx, Msg::SuccessfulCreation)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::SuccessfulCreation => {
 | 
			
		||||
                self.route_dispatcher
 | 
			
		||||
                    .send(RouteRequest::ChangeRoute(Route::from(AppRoute::ListUsers)));
 | 
			
		||||
                ctx.link().history().unwrap().push(AppRoute::ListUsers);
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -173,23 +170,19 @@ impl Component for CreateUserForm {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = ();
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(_: &Context<Self>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            route_dispatcher: RouteAgentDispatcher::new(),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            form: yew_form::Form::<CreateUserModel>::new(CreateUserModel::default()),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        type Field = yew_form::Field<CreateUserModel>;
 | 
			
		||||
        html! {
 | 
			
		||||
          <div class="row justify-content-center">
 | 
			
		||||
@ -206,13 +199,13 @@ impl Component for CreateUserForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="username"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    autocomplete="username"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("username")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -227,14 +220,14 @@ impl Component for CreateUserForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    input_type="email"
 | 
			
		||||
                    field_name="email"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    autocomplete="email"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("email")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -247,13 +240,13 @@ impl Component for CreateUserForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    autocomplete="name"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    field_name="display_name"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("display_name")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -266,13 +259,13 @@ impl Component for CreateUserForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    autocomplete="given-name"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    field_name="first_name"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("first_name")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -285,13 +278,13 @@ impl Component for CreateUserForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    autocomplete="family-name"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    field_name="last_name"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("last_name")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -304,14 +297,14 @@ impl Component for CreateUserForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    input_type="password"
 | 
			
		||||
                    field_name="password"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    autocomplete="new-password"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("password")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -324,14 +317,14 @@ impl Component for CreateUserForm {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    input_type="password"
 | 
			
		||||
                    field_name="confirm_password"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    autocomplete="new-password"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("confirm_password")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -340,9 +333,9 @@ impl Component for CreateUserForm {
 | 
			
		||||
              <div class="form-group row justify-content-center">
 | 
			
		||||
                <button
 | 
			
		||||
                  class="btn btn-primary col-auto col-form-label mt-4"
 | 
			
		||||
                  disabled=self.common.is_task_running()
 | 
			
		||||
                  disabled={self.common.is_task_running()}
 | 
			
		||||
                  type="submit"
 | 
			
		||||
                  onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})>
 | 
			
		||||
                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitForm})}>
 | 
			
		||||
                  <i class="bi-save me-2"></i>
 | 
			
		||||
                  {"Submit"}
 | 
			
		||||
                </button>
 | 
			
		||||
 | 
			
		||||
@ -39,16 +39,21 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<DeleteGroup> for DeleteGroup {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::ClickedDeleteGroup => {
 | 
			
		||||
                self.modal.as_ref().expect("modal not initialized").show();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::ConfirmDeleteGroup => {
 | 
			
		||||
                self.update(Msg::DismissModal);
 | 
			
		||||
                self.update(ctx, Msg::DismissModal);
 | 
			
		||||
                self.common.call_graphql::<DeleteGroupQuery, _>(
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    delete_group_query::Variables {
 | 
			
		||||
                        group_id: self.common.group.id,
 | 
			
		||||
                        group_id: ctx.props().group.id,
 | 
			
		||||
                    },
 | 
			
		||||
                    Msg::DeleteGroupResponse,
 | 
			
		||||
                    "Error trying to delete group",
 | 
			
		||||
@ -58,12 +63,8 @@ impl CommonComponent<DeleteGroup> for DeleteGroup {
 | 
			
		||||
                self.modal.as_ref().expect("modal not initialized").hide();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::DeleteGroupResponse(response) => {
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                response?;
 | 
			
		||||
                self.common
 | 
			
		||||
                    .props
 | 
			
		||||
                    .on_group_deleted
 | 
			
		||||
                    .emit(self.common.group.id);
 | 
			
		||||
                ctx.props().on_group_deleted.emit(ctx.props().group.id);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(true)
 | 
			
		||||
@ -78,15 +79,15 @@ impl Component for DeleteGroup {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = DeleteGroupProps;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(_: &Context<Self>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            node_ref: NodeRef::default(),
 | 
			
		||||
            modal: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn rendered(&mut self, first_render: bool) {
 | 
			
		||||
    fn rendered(&mut self, _: &Context<Self>, first_render: bool) {
 | 
			
		||||
        if first_render {
 | 
			
		||||
            self.modal = Some(Modal::new(
 | 
			
		||||
                self.node_ref
 | 
			
		||||
@ -96,43 +97,42 @@ impl Component for DeleteGroup {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update_and_report_error(
 | 
			
		||||
            self,
 | 
			
		||||
            ctx,
 | 
			
		||||
            msg,
 | 
			
		||||
            self.common.on_error.clone(),
 | 
			
		||||
            ctx.props().on_error.clone(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        html! {
 | 
			
		||||
          <>
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-danger"
 | 
			
		||||
            disabled=self.common.is_task_running()
 | 
			
		||||
            onclick=self.common.callback(|_| Msg::ClickedDeleteGroup)>
 | 
			
		||||
            disabled={self.common.is_task_running()}
 | 
			
		||||
            onclick={link.callback(|_| Msg::ClickedDeleteGroup)}>
 | 
			
		||||
            <i class="bi-x-circle-fill" aria-label="Delete group" />
 | 
			
		||||
          </button>
 | 
			
		||||
          {self.show_modal()}
 | 
			
		||||
          {self.show_modal(ctx)}
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl DeleteGroup {
 | 
			
		||||
    fn show_modal(&self) -> Html {
 | 
			
		||||
    fn show_modal(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        html! {
 | 
			
		||||
          <div
 | 
			
		||||
            class="modal fade"
 | 
			
		||||
            id="deleteGroupModal".to_string() + &self.common.group.id.to_string()
 | 
			
		||||
            id={"deleteGroupModal".to_string() + &ctx.props().group.id.to_string()}
 | 
			
		||||
            tabindex="-1"
 | 
			
		||||
            aria-labelledby="deleteGroupModalLabel"
 | 
			
		||||
            aria-hidden="true"
 | 
			
		||||
            ref=self.node_ref.clone()>
 | 
			
		||||
            ref={self.node_ref.clone()}>
 | 
			
		||||
            <div class="modal-dialog">
 | 
			
		||||
              <div class="modal-content">
 | 
			
		||||
                <div class="modal-header">
 | 
			
		||||
@ -141,25 +141,25 @@ impl DeleteGroup {
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    class="btn-close"
 | 
			
		||||
                    aria-label="Close"
 | 
			
		||||
                    onclick=self.common.callback(|_| Msg::DismissModal) />
 | 
			
		||||
                    onclick={link.callback(|_| Msg::DismissModal)} />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                <span>
 | 
			
		||||
                  {"Are you sure you want to delete group "}
 | 
			
		||||
                  <b>{&self.common.group.display_name}</b>{"?"}
 | 
			
		||||
                  <b>{&ctx.props().group.display_name}</b>{"?"}
 | 
			
		||||
                </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    class="btn btn-secondary"
 | 
			
		||||
                    onclick=self.common.callback(|_| Msg::DismissModal)>
 | 
			
		||||
                    onclick={link.callback(|_| Msg::DismissModal)}>
 | 
			
		||||
                      <i class="bi-x-circle me-2"></i>
 | 
			
		||||
                      {"Cancel"}
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    onclick=self.common.callback(|_| Msg::ConfirmDeleteGroup)
 | 
			
		||||
                    onclick={link.callback(|_| Msg::ConfirmDeleteGroup)}
 | 
			
		||||
                    class="btn btn-danger">
 | 
			
		||||
                    <i class="bi-check-circle me-2"></i>
 | 
			
		||||
                    {"Yes, I'm sure"}
 | 
			
		||||
 | 
			
		||||
@ -36,16 +36,21 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<DeleteUser> for DeleteUser {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::ClickedDeleteUser => {
 | 
			
		||||
                self.modal.as_ref().expect("modal not initialized").show();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::ConfirmDeleteUser => {
 | 
			
		||||
                self.update(Msg::DismissModal);
 | 
			
		||||
                self.update(ctx, Msg::DismissModal);
 | 
			
		||||
                self.common.call_graphql::<DeleteUserQuery, _>(
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    delete_user_query::Variables {
 | 
			
		||||
                        user: self.common.username.clone(),
 | 
			
		||||
                        user: ctx.props().username.clone(),
 | 
			
		||||
                    },
 | 
			
		||||
                    Msg::DeleteUserResponse,
 | 
			
		||||
                    "Error trying to delete user",
 | 
			
		||||
@ -55,12 +60,10 @@ impl CommonComponent<DeleteUser> for DeleteUser {
 | 
			
		||||
                self.modal.as_ref().expect("modal not initialized").hide();
 | 
			
		||||
            }
 | 
			
		||||
            Msg::DeleteUserResponse(response) => {
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                response?;
 | 
			
		||||
                self.common
 | 
			
		||||
                    .props
 | 
			
		||||
                ctx.props()
 | 
			
		||||
                    .on_user_deleted
 | 
			
		||||
                    .emit(self.common.username.clone());
 | 
			
		||||
                    .emit(ctx.props().username.clone());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(true)
 | 
			
		||||
@ -75,15 +78,15 @@ impl Component for DeleteUser {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = DeleteUserProps;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(_: &Context<Self>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            node_ref: NodeRef::default(),
 | 
			
		||||
            modal: None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn rendered(&mut self, first_render: bool) {
 | 
			
		||||
    fn rendered(&mut self, _: &Context<Self>, first_render: bool) {
 | 
			
		||||
        if first_render {
 | 
			
		||||
            self.modal = Some(Modal::new(
 | 
			
		||||
                self.node_ref
 | 
			
		||||
@ -93,44 +96,43 @@ impl Component for DeleteUser {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update_and_report_error(
 | 
			
		||||
            self,
 | 
			
		||||
            ctx,
 | 
			
		||||
            msg,
 | 
			
		||||
            self.common.on_error.clone(),
 | 
			
		||||
            ctx.props().on_error.clone(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        html! {
 | 
			
		||||
          <>
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-danger"
 | 
			
		||||
            disabled=self.common.is_task_running()
 | 
			
		||||
            onclick=self.common.callback(|_| Msg::ClickedDeleteUser)>
 | 
			
		||||
            disabled={self.common.is_task_running()}
 | 
			
		||||
            onclick={link.callback(|_| Msg::ClickedDeleteUser)}>
 | 
			
		||||
            <i class="bi-x-circle-fill" aria-label="Delete user" />
 | 
			
		||||
          </button>
 | 
			
		||||
          {self.show_modal()}
 | 
			
		||||
          {self.show_modal(ctx)}
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl DeleteUser {
 | 
			
		||||
    fn show_modal(&self) -> Html {
 | 
			
		||||
    fn show_modal(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        html! {
 | 
			
		||||
          <div
 | 
			
		||||
            class="modal fade"
 | 
			
		||||
            id="deleteUserModal".to_string() + &self.common.username
 | 
			
		||||
            id={"deleteUserModal".to_string() + &ctx.props().username}
 | 
			
		||||
            tabindex="-1"
 | 
			
		||||
            //role="dialog"
 | 
			
		||||
            aria-labelledby="deleteUserModalLabel"
 | 
			
		||||
            aria-hidden="true"
 | 
			
		||||
            ref=self.node_ref.clone()>
 | 
			
		||||
            ref={self.node_ref.clone()}>
 | 
			
		||||
            <div class="modal-dialog" /*role="document"*/>
 | 
			
		||||
              <div class="modal-content">
 | 
			
		||||
                <div class="modal-header">
 | 
			
		||||
@ -139,25 +141,25 @@ impl DeleteUser {
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    class="btn-close"
 | 
			
		||||
                    aria-label="Close"
 | 
			
		||||
                    onclick=self.common.callback(|_| Msg::DismissModal) />
 | 
			
		||||
                    onclick={link.callback(|_| Msg::DismissModal)} />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-body">
 | 
			
		||||
                <span>
 | 
			
		||||
                  {"Are you sure you want to delete user "}
 | 
			
		||||
                  <b>{&self.common.username}</b>{"?"}
 | 
			
		||||
                  <b>{&ctx.props().username}</b>{"?"}
 | 
			
		||||
                </span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="modal-footer">
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    class="btn btn-secondary"
 | 
			
		||||
                    onclick=self.common.callback(|_| Msg::DismissModal)>
 | 
			
		||||
                    onclick={link.callback(|_| Msg::DismissModal)}>
 | 
			
		||||
                    <i class="bi-x-circle me-2"></i>
 | 
			
		||||
                    {"Cancel"}
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    onclick=self.common.callback(|_| Msg::ConfirmDeleteUser)
 | 
			
		||||
                    onclick={link.callback(|_| Msg::ConfirmDeleteUser)}
 | 
			
		||||
                    class="btn btn-danger">
 | 
			
		||||
                    <i class="bi-check-circle me-2"></i>
 | 
			
		||||
                    {"Yes, I'm sure"}
 | 
			
		||||
 | 
			
		||||
@ -46,10 +46,11 @@ pub struct Props {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GroupDetails {
 | 
			
		||||
    fn get_group_details(&mut self) {
 | 
			
		||||
    fn get_group_details(&mut self, ctx: &Context<Self>) {
 | 
			
		||||
        self.common.call_graphql::<GetGroupDetails, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            get_group_details::Variables {
 | 
			
		||||
                id: self.common.group_id,
 | 
			
		||||
                id: ctx.props().group_id,
 | 
			
		||||
            },
 | 
			
		||||
            Msg::GroupDetailsResponse,
 | 
			
		||||
            "Error trying to fetch group details",
 | 
			
		||||
@ -107,24 +108,25 @@ impl GroupDetails {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_user_list(&self, g: &Group) -> Html {
 | 
			
		||||
    fn view_user_list(&self, ctx: &Context<Self>, g: &Group) -> Html {
 | 
			
		||||
        let link = ctx.link();
 | 
			
		||||
        let make_user_row = |user: &User| {
 | 
			
		||||
            let user_id = user.id.clone();
 | 
			
		||||
            let display_name = user.display_name.clone();
 | 
			
		||||
            html! {
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <Link route=AppRoute::UserDetails(user_id.clone())>
 | 
			
		||||
                  <Link to={AppRoute::UserDetails{user_id: user_id.clone()}}>
 | 
			
		||||
                    {user_id.clone()}
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>{display_name}</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <RemoveUserFromGroupComponent
 | 
			
		||||
                    username=user_id
 | 
			
		||||
                    group_id=g.id
 | 
			
		||||
                    on_user_removed_from_group=self.common.callback(Msg::OnUserRemovedFromGroup)
 | 
			
		||||
                    on_error=self.common.callback(Msg::OnError)/>
 | 
			
		||||
                    username={user_id}
 | 
			
		||||
                    group_id={g.id}
 | 
			
		||||
                    on_user_removed_from_group={link.callback(Msg::OnUserRemovedFromGroup)}
 | 
			
		||||
                    on_error={link.callback(Msg::OnError)}/>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            }
 | 
			
		||||
@ -159,7 +161,8 @@ impl GroupDetails {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_add_user_button(&self, g: &Group) -> Html {
 | 
			
		||||
    fn view_add_user_button(&self, ctx: &Context<Self>, g: &Group) -> Html {
 | 
			
		||||
        let link = ctx.link();
 | 
			
		||||
        let users: Vec<_> = g
 | 
			
		||||
            .users
 | 
			
		||||
            .iter()
 | 
			
		||||
@ -170,16 +173,16 @@ impl GroupDetails {
 | 
			
		||||
            .collect();
 | 
			
		||||
        html! {
 | 
			
		||||
            <AddGroupMemberComponent
 | 
			
		||||
                group_id=g.id
 | 
			
		||||
                users=users
 | 
			
		||||
                on_error=self.common.callback(Msg::OnError)
 | 
			
		||||
                on_user_added_to_group=self.common.callback(Msg::OnUserAddedToGroup)/>
 | 
			
		||||
                group_id={g.id}
 | 
			
		||||
                users={users}
 | 
			
		||||
                on_error={link.callback(Msg::OnError)}
 | 
			
		||||
                on_user_added_to_group={link.callback(Msg::OnUserAddedToGroup)}/>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<GroupDetails> for GroupDetails {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::GroupDetailsResponse(response) => match response {
 | 
			
		||||
                Ok(group) => self.group = Some(group.group),
 | 
			
		||||
@ -215,24 +218,20 @@ impl Component for GroupDetails {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let mut table = Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            group: None,
 | 
			
		||||
        };
 | 
			
		||||
        table.get_group_details();
 | 
			
		||||
        table.get_group_details(ctx);
 | 
			
		||||
        table
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        match (&self.group, &self.common.error) {
 | 
			
		||||
            (None, None) => html! {{"Loading..."}},
 | 
			
		||||
            (None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
 | 
			
		||||
@ -240,8 +239,8 @@ impl Component for GroupDetails {
 | 
			
		||||
                html! {
 | 
			
		||||
                    <div>
 | 
			
		||||
                      {self.view_details(u)}
 | 
			
		||||
                      {self.view_user_list(u)}
 | 
			
		||||
                      {self.view_add_user_button(u)}
 | 
			
		||||
                      {self.view_user_list(ctx, u)}
 | 
			
		||||
                      {self.view_add_user_button(ctx, u)}
 | 
			
		||||
                      {self.view_messages(error)}
 | 
			
		||||
                    </div>
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<GroupTable> for GroupTable {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::ListGroupsResponse(groups) => {
 | 
			
		||||
                self.groups = Some(groups?.groups.into_iter().collect());
 | 
			
		||||
@ -58,12 +58,13 @@ impl Component for GroupTable {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = ();
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let mut table = GroupTable {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            groups: None,
 | 
			
		||||
        };
 | 
			
		||||
        table.common.call_graphql::<GetGroupList, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            get_group_list::Variables {},
 | 
			
		||||
            Msg::ListGroupsResponse,
 | 
			
		||||
            "Error trying to fetch groups",
 | 
			
		||||
@ -71,18 +72,14 @@ impl Component for GroupTable {
 | 
			
		||||
        table
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
            <div>
 | 
			
		||||
              {self.view_groups()}
 | 
			
		||||
              {self.view_groups(ctx)}
 | 
			
		||||
              {self.view_errors()}
 | 
			
		||||
            </div>
 | 
			
		||||
        }
 | 
			
		||||
@ -90,7 +87,7 @@ impl Component for GroupTable {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl GroupTable {
 | 
			
		||||
    fn view_groups(&self) -> Html {
 | 
			
		||||
    fn view_groups(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let make_table = |groups: &Vec<Group>| {
 | 
			
		||||
            html! {
 | 
			
		||||
                <div class="table-responsive">
 | 
			
		||||
@ -103,7 +100,7 @@ impl GroupTable {
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                      {groups.iter().map(|u| self.view_group(u)).collect::<Vec<_>>()}
 | 
			
		||||
                      {groups.iter().map(|u| self.view_group(ctx, u)).collect::<Vec<_>>()}
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                  </table>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -115,11 +112,12 @@ impl GroupTable {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_group(&self, group: &Group) -> Html {
 | 
			
		||||
    fn view_group(&self, ctx: &Context<Self>, group: &Group) -> Html {
 | 
			
		||||
        let link = ctx.link();
 | 
			
		||||
        html! {
 | 
			
		||||
          <tr key=group.id>
 | 
			
		||||
          <tr key={group.id}>
 | 
			
		||||
              <td>
 | 
			
		||||
                <Link route=AppRoute::GroupDetails(group.id)>
 | 
			
		||||
                <Link to={AppRoute::GroupDetails{group_id: group.id}}>
 | 
			
		||||
                  {&group.display_name}
 | 
			
		||||
                </Link>
 | 
			
		||||
              </td>
 | 
			
		||||
@ -128,9 +126,9 @@ impl GroupTable {
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <DeleteGroup
 | 
			
		||||
                  group=group.clone()
 | 
			
		||||
                  on_group_deleted=self.common.callback(Msg::OnGroupDeleted)
 | 
			
		||||
                  on_error=self.common.callback(Msg::OnError)/>
 | 
			
		||||
                  group={group.clone()}
 | 
			
		||||
                  on_group_deleted={link.callback(Msg::OnGroupDeleted)}
 | 
			
		||||
                  on_error={link.callback(Msg::OnError)}/>
 | 
			
		||||
              </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,15 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    components::router::{AppRoute, NavButton},
 | 
			
		||||
    components::router::{AppRoute, Link},
 | 
			
		||||
    infra::{
 | 
			
		||||
        api::HostService,
 | 
			
		||||
        common_component::{CommonComponent, CommonComponentParts},
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{anyhow, bail, Context, Result};
 | 
			
		||||
use anyhow::{anyhow, bail, Result};
 | 
			
		||||
use gloo_console::error;
 | 
			
		||||
use lldap_auth::*;
 | 
			
		||||
use validator_derive::Validate;
 | 
			
		||||
use yew::{prelude::*, services::ConsoleService};
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
use yew_form::Form;
 | 
			
		||||
use yew_form_derive::Model;
 | 
			
		||||
 | 
			
		||||
@ -47,7 +48,12 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<LoginForm> for LoginForm {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        use anyhow::Context;
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::Update => Ok(true),
 | 
			
		||||
            Msg::Submit => {
 | 
			
		||||
@ -64,9 +70,9 @@ impl CommonComponent<LoginForm> for LoginForm {
 | 
			
		||||
                    login_start_request: message,
 | 
			
		||||
                };
 | 
			
		||||
                self.common
 | 
			
		||||
                    .call_backend(HostService::login_start, req, move |r| {
 | 
			
		||||
                    .call_backend(ctx, HostService::login_start(req), move |r| {
 | 
			
		||||
                        Msg::AuthenticationStartResponse((state, r))
 | 
			
		||||
                    })?;
 | 
			
		||||
                    });
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::AuthenticationStartResponse((login_start, res)) => {
 | 
			
		||||
@ -77,9 +83,8 @@ impl CommonComponent<LoginForm> for LoginForm {
 | 
			
		||||
                        Err(e) => {
 | 
			
		||||
                            // Common error, we want to print a full error to the console but only a
 | 
			
		||||
                            // simple one to the user.
 | 
			
		||||
                            ConsoleService::error(&format!("Invalid username or password: {}", e));
 | 
			
		||||
                            error!(&format!("Invalid username or password: {}", e));
 | 
			
		||||
                            self.common.error = Some(anyhow!("Invalid username or password"));
 | 
			
		||||
                            self.common.cancel_task();
 | 
			
		||||
                            return Ok(true);
 | 
			
		||||
                        }
 | 
			
		||||
                        Ok(l) => l,
 | 
			
		||||
@ -89,24 +94,22 @@ impl CommonComponent<LoginForm> for LoginForm {
 | 
			
		||||
                    credential_finalization: login_finish.message,
 | 
			
		||||
                };
 | 
			
		||||
                self.common.call_backend(
 | 
			
		||||
                    HostService::login_finish,
 | 
			
		||||
                    req,
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    HostService::login_finish(req),
 | 
			
		||||
                    Msg::AuthenticationFinishResponse,
 | 
			
		||||
                )?;
 | 
			
		||||
                );
 | 
			
		||||
                Ok(false)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::AuthenticationFinishResponse(user_info) => {
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                self.common
 | 
			
		||||
                ctx.props()
 | 
			
		||||
                    .on_logged_in
 | 
			
		||||
                    .emit(user_info.context("Could not log in")?);
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::AuthenticationRefreshResponse(user_info) => {
 | 
			
		||||
                self.refreshing = false;
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                if let Ok(user_info) = user_info {
 | 
			
		||||
                    self.common.on_logged_in.emit(user_info);
 | 
			
		||||
                    ctx.props().on_logged_in.emit(user_info);
 | 
			
		||||
                }
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
@ -122,33 +125,28 @@ impl Component for LoginForm {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let mut app = LoginForm {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            form: Form::<FormModel>::new(FormModel::default()),
 | 
			
		||||
            refreshing: true,
 | 
			
		||||
        };
 | 
			
		||||
        if let Err(e) =
 | 
			
		||||
            app.common
 | 
			
		||||
                .call_backend(HostService::refresh, (), Msg::AuthenticationRefreshResponse)
 | 
			
		||||
        {
 | 
			
		||||
            ConsoleService::debug(&format!("Could not refresh auth: {}", e));
 | 
			
		||||
            app.refreshing = false;
 | 
			
		||||
        }
 | 
			
		||||
        app.common.call_backend(
 | 
			
		||||
            ctx,
 | 
			
		||||
            HostService::refresh(),
 | 
			
		||||
            Msg::AuthenticationRefreshResponse,
 | 
			
		||||
        );
 | 
			
		||||
        app
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        type Field = yew_form::Field<FormModel>;
 | 
			
		||||
        let password_reset_enabled = self.common.password_reset_enabled;
 | 
			
		||||
        let password_reset_enabled = ctx.props().password_reset_enabled;
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        if self.refreshing {
 | 
			
		||||
            html! {
 | 
			
		||||
              <div>
 | 
			
		||||
@ -169,11 +167,11 @@ impl Component for LoginForm {
 | 
			
		||||
                      class="form-control"
 | 
			
		||||
                      class_invalid="is-invalid has-error"
 | 
			
		||||
                      class_valid="has-success"
 | 
			
		||||
                      form=&self.form
 | 
			
		||||
                      form={&self.form}
 | 
			
		||||
                      field_name="username"
 | 
			
		||||
                      placeholder="Username"
 | 
			
		||||
                      autocomplete="username"
 | 
			
		||||
                      oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                      oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div class="input-group">
 | 
			
		||||
                    <div class="input-group-prepend">
 | 
			
		||||
@ -185,7 +183,7 @@ impl Component for LoginForm {
 | 
			
		||||
                      class="form-control"
 | 
			
		||||
                      class_invalid="is-invalid has-error"
 | 
			
		||||
                      class_valid="has-success"
 | 
			
		||||
                      form=&self.form
 | 
			
		||||
                      form={&self.form}
 | 
			
		||||
                      field_name="password"
 | 
			
		||||
                      input_type="password"
 | 
			
		||||
                      placeholder="Password"
 | 
			
		||||
@ -195,19 +193,19 @@ impl Component for LoginForm {
 | 
			
		||||
                    <button
 | 
			
		||||
                      type="submit"
 | 
			
		||||
                      class="btn btn-primary"
 | 
			
		||||
                      disabled=self.common.is_task_running()
 | 
			
		||||
                      onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
 | 
			
		||||
                      disabled={self.common.is_task_running()}
 | 
			
		||||
                      onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
 | 
			
		||||
                      <i class="bi-box-arrow-in-right me-2"/>
 | 
			
		||||
                      {"Login"}
 | 
			
		||||
                    </button>
 | 
			
		||||
                    { if password_reset_enabled {
 | 
			
		||||
                      html! {
 | 
			
		||||
                        <NavButton
 | 
			
		||||
                        <Link
 | 
			
		||||
                          classes="btn-link btn"
 | 
			
		||||
                          disabled=self.common.is_task_running()
 | 
			
		||||
                          route=AppRoute::StartResetPassword>
 | 
			
		||||
                          disabled={self.common.is_task_running()}
 | 
			
		||||
                          to={AppRoute::StartResetPassword}>
 | 
			
		||||
                          {"Forgot your password?"}
 | 
			
		||||
                        </NavButton>
 | 
			
		||||
                        </Link>
 | 
			
		||||
                      }
 | 
			
		||||
                    } else {
 | 
			
		||||
                      html!{}
 | 
			
		||||
 | 
			
		||||
@ -21,16 +21,20 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<LogoutButton> for LogoutButton {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::LogoutRequested => {
 | 
			
		||||
                self.common
 | 
			
		||||
                    .call_backend(HostService::logout, (), Msg::LogoutCompleted)?;
 | 
			
		||||
                    .call_backend(ctx, HostService::logout(), Msg::LogoutCompleted);
 | 
			
		||||
            }
 | 
			
		||||
            Msg::LogoutCompleted(res) => {
 | 
			
		||||
                res?;
 | 
			
		||||
                delete_cookie("user_id")?;
 | 
			
		||||
                self.common.on_logged_out.emit(());
 | 
			
		||||
                ctx.props().on_logged_out.emit(());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(false)
 | 
			
		||||
@ -45,25 +49,22 @@ impl Component for LogoutButton {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(_: &Context<Self>) -> Self {
 | 
			
		||||
        LogoutButton {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        html! {
 | 
			
		||||
            <button
 | 
			
		||||
              class="dropdown-item"
 | 
			
		||||
              onclick=self.common.callback(|_| Msg::LogoutRequested)>
 | 
			
		||||
              onclick={link.callback(|_| Msg::LogoutRequested)}>
 | 
			
		||||
              {"Logout"}
 | 
			
		||||
            </button>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -31,15 +31,18 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<RemoveUserFromGroupComponent> for RemoveUserFromGroupComponent {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::SubmitRemoveGroup => self.submit_remove_group(),
 | 
			
		||||
            Msg::SubmitRemoveGroup => self.submit_remove_group(ctx),
 | 
			
		||||
            Msg::RemoveGroupResponse(response) => {
 | 
			
		||||
                response?;
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                self.common
 | 
			
		||||
                ctx.props()
 | 
			
		||||
                    .on_user_removed_from_group
 | 
			
		||||
                    .emit((self.common.username.clone(), self.common.group_id));
 | 
			
		||||
                    .emit((ctx.props().username.clone(), ctx.props().group_id));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        Ok(true)
 | 
			
		||||
@ -51,11 +54,12 @@ impl CommonComponent<RemoveUserFromGroupComponent> for RemoveUserFromGroupCompon
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl RemoveUserFromGroupComponent {
 | 
			
		||||
    fn submit_remove_group(&mut self) {
 | 
			
		||||
    fn submit_remove_group(&mut self, ctx: &Context<Self>) {
 | 
			
		||||
        self.common.call_graphql::<RemoveUserFromGroup, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            remove_user_from_group::Variables {
 | 
			
		||||
                user: self.common.username.clone(),
 | 
			
		||||
                group: self.common.group_id,
 | 
			
		||||
                user: ctx.props().username.clone(),
 | 
			
		||||
                group: ctx.props().group_id,
 | 
			
		||||
            },
 | 
			
		||||
            Msg::RemoveGroupResponse,
 | 
			
		||||
            "Error trying to initiate removing the user from a group",
 | 
			
		||||
@ -67,30 +71,28 @@ impl Component for RemoveUserFromGroupComponent {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(_: &Context<Self>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update_and_report_error(
 | 
			
		||||
            self,
 | 
			
		||||
            ctx,
 | 
			
		||||
            msg,
 | 
			
		||||
            self.common.on_error.clone(),
 | 
			
		||||
            ctx.props().on_error.clone(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        html! {
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-danger"
 | 
			
		||||
            disabled=self.common.is_task_running()
 | 
			
		||||
            onclick=self.common.callback(|_| Msg::SubmitRemoveGroup)>
 | 
			
		||||
            disabled={self.common.is_task_running()}
 | 
			
		||||
            onclick={link.callback(|_| Msg::SubmitRemoveGroup)}>
 | 
			
		||||
            <i class="bi-x-circle-fill" aria-label="Remove user from group" />
 | 
			
		||||
          </button>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    components::router::{AppRoute, NavButton},
 | 
			
		||||
    components::router::{AppRoute, Link},
 | 
			
		||||
    infra::{
 | 
			
		||||
        api::HostService,
 | 
			
		||||
        common_component::{CommonComponent, CommonComponentParts},
 | 
			
		||||
@ -31,7 +31,11 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<ResetPasswordStep1Form> for ResetPasswordStep1Form {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::Update => Ok(true),
 | 
			
		||||
            Msg::Submit => {
 | 
			
		||||
@ -40,10 +44,10 @@ impl CommonComponent<ResetPasswordStep1Form> for ResetPasswordStep1Form {
 | 
			
		||||
                }
 | 
			
		||||
                let FormModel { username } = self.form.model();
 | 
			
		||||
                self.common.call_backend(
 | 
			
		||||
                    HostService::reset_password_step1,
 | 
			
		||||
                    &username,
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    HostService::reset_password_step1(username),
 | 
			
		||||
                    Msg::PasswordResetResponse,
 | 
			
		||||
                )?;
 | 
			
		||||
                );
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::PasswordResetResponse(response) => {
 | 
			
		||||
@ -63,25 +67,22 @@ impl Component for ResetPasswordStep1Form {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = ();
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(_: &Context<Self>) -> Self {
 | 
			
		||||
        ResetPasswordStep1Form {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            form: Form::<FormModel>::new(FormModel::default()),
 | 
			
		||||
            just_succeeded: false,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        self.just_succeeded = false;
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        type Field = yew_form::Field<FormModel>;
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        html! {
 | 
			
		||||
            <form
 | 
			
		||||
              class="form center-block col-sm-4 col-offset-4">
 | 
			
		||||
@ -95,11 +96,11 @@ impl Component for ResetPasswordStep1Form {
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="username"
 | 
			
		||||
                    placeholder="Username or email"
 | 
			
		||||
                    autocomplete="username"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                </div>
 | 
			
		||||
                { if self.just_succeeded {
 | 
			
		||||
                    html! {
 | 
			
		||||
@ -111,17 +112,17 @@ impl Component for ResetPasswordStep1Form {
 | 
			
		||||
                          <button
 | 
			
		||||
                            type="submit"
 | 
			
		||||
                            class="btn btn-primary"
 | 
			
		||||
                            disabled=self.common.is_task_running()
 | 
			
		||||
                            onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
 | 
			
		||||
                            disabled={self.common.is_task_running()}
 | 
			
		||||
                            onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
 | 
			
		||||
                            <i class="bi-check-circle me-2"/>
 | 
			
		||||
                            {"Reset password"}
 | 
			
		||||
                          </button>
 | 
			
		||||
                          <NavButton
 | 
			
		||||
                          <Link
 | 
			
		||||
                            classes="btn-link btn"
 | 
			
		||||
                            disabled=self.common.is_task_running()
 | 
			
		||||
                            route=AppRoute::Login>
 | 
			
		||||
                            disabled={self.common.is_task_running()}
 | 
			
		||||
                            to={AppRoute::Login}>
 | 
			
		||||
                            {"Back"}
 | 
			
		||||
                          </NavButton>
 | 
			
		||||
                          </Link>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    }
 | 
			
		||||
                }}
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
use crate::{
 | 
			
		||||
    components::router::{AppRoute, NavButton},
 | 
			
		||||
    components::router::{AppRoute, Link},
 | 
			
		||||
    infra::{
 | 
			
		||||
        api::HostService,
 | 
			
		||||
        common_component::{CommonComponent, CommonComponentParts},
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{bail, Context, Result};
 | 
			
		||||
use anyhow::{bail, Result};
 | 
			
		||||
use lldap_auth::{
 | 
			
		||||
    opaque::client::registration as opaque_registration,
 | 
			
		||||
    password_reset::ServerPasswordResetResponse, registration,
 | 
			
		||||
@ -14,10 +14,7 @@ use validator_derive::Validate;
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
use yew_form::Form;
 | 
			
		||||
use yew_form_derive::Model;
 | 
			
		||||
use yew_router::{
 | 
			
		||||
    agent::{RouteAgentDispatcher, RouteRequest},
 | 
			
		||||
    route::Route,
 | 
			
		||||
};
 | 
			
		||||
use yew_router::{prelude::History, scope_ext::RouterScopeExt};
 | 
			
		||||
 | 
			
		||||
/// The fields of the form, with the constraints.
 | 
			
		||||
#[derive(Model, Validate, PartialEq, Eq, Clone, Default)]
 | 
			
		||||
@ -33,7 +30,6 @@ pub struct ResetPasswordStep2Form {
 | 
			
		||||
    form: Form<FormModel>,
 | 
			
		||||
    username: Option<String>,
 | 
			
		||||
    opaque_data: Option<opaque_registration::ClientRegistration>,
 | 
			
		||||
    route_dispatcher: RouteAgentDispatcher,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Clone, PartialEq, Eq, Properties)]
 | 
			
		||||
@ -50,11 +46,15 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        use anyhow::Context;
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::ValidateTokenResponse(response) => {
 | 
			
		||||
                self.username = Some(response?.user_id);
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::FormUpdate => Ok(true),
 | 
			
		||||
@ -73,10 +73,10 @@ impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
 | 
			
		||||
                };
 | 
			
		||||
                self.opaque_data = Some(registration_start_request.state);
 | 
			
		||||
                self.common.call_backend(
 | 
			
		||||
                    HostService::register_start,
 | 
			
		||||
                    req,
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    HostService::register_start(req),
 | 
			
		||||
                    Msg::RegistrationStartResponse,
 | 
			
		||||
                )?;
 | 
			
		||||
                );
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::RegistrationStartResponse(res) => {
 | 
			
		||||
@ -94,17 +94,15 @@ impl CommonComponent<ResetPasswordStep2Form> for ResetPasswordStep2Form {
 | 
			
		||||
                    registration_upload: registration_finish.message,
 | 
			
		||||
                };
 | 
			
		||||
                self.common.call_backend(
 | 
			
		||||
                    HostService::register_finish,
 | 
			
		||||
                    req,
 | 
			
		||||
                    ctx,
 | 
			
		||||
                    HostService::register_finish(req),
 | 
			
		||||
                    Msg::RegistrationFinishResponse,
 | 
			
		||||
                )?;
 | 
			
		||||
                );
 | 
			
		||||
                Ok(false)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::RegistrationFinishResponse(response) => {
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
                if response.is_ok() {
 | 
			
		||||
                    self.route_dispatcher
 | 
			
		||||
                        .send(RouteRequest::ChangeRoute(Route::from(AppRoute::Login)));
 | 
			
		||||
                    ctx.link().history().unwrap().push(AppRoute::Login);
 | 
			
		||||
                }
 | 
			
		||||
                response?;
 | 
			
		||||
                Ok(true)
 | 
			
		||||
@ -121,35 +119,28 @@ impl Component for ResetPasswordStep2Form {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let mut component = ResetPasswordStep2Form {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            form: yew_form::Form::<FormModel>::new(FormModel::default()),
 | 
			
		||||
            opaque_data: None,
 | 
			
		||||
            route_dispatcher: RouteAgentDispatcher::new(),
 | 
			
		||||
            username: None,
 | 
			
		||||
        };
 | 
			
		||||
        let token = component.common.token.clone();
 | 
			
		||||
        component
 | 
			
		||||
            .common
 | 
			
		||||
            .call_backend(
 | 
			
		||||
                HostService::reset_password_step2,
 | 
			
		||||
                &token,
 | 
			
		||||
        let token = ctx.props().token.clone();
 | 
			
		||||
        component.common.call_backend(
 | 
			
		||||
            ctx,
 | 
			
		||||
            HostService::reset_password_step2(token),
 | 
			
		||||
            Msg::ValidateTokenResponse,
 | 
			
		||||
            )
 | 
			
		||||
            .unwrap();
 | 
			
		||||
        );
 | 
			
		||||
        component
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        match (&self.username, &self.common.error) {
 | 
			
		||||
            (None, None) => {
 | 
			
		||||
                return html! {
 | 
			
		||||
@ -162,12 +153,12 @@ impl Component for ResetPasswordStep2Form {
 | 
			
		||||
                    <div class="alert alert-danger">
 | 
			
		||||
                      {e.to_string() }
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <NavButton
 | 
			
		||||
                    <Link
 | 
			
		||||
                      classes="btn-link btn"
 | 
			
		||||
                      disabled=self.common.is_task_running()
 | 
			
		||||
                      route=AppRoute::Login>
 | 
			
		||||
                      disabled={self.common.is_task_running()}
 | 
			
		||||
                      to={AppRoute::Login}>
 | 
			
		||||
                      {"Back"}
 | 
			
		||||
                    </NavButton>
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </>
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@ -186,14 +177,14 @@ impl Component for ResetPasswordStep2Form {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-sm-10">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="password"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    autocomplete="new-password"
 | 
			
		||||
                    input_type="password"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::FormUpdate) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::FormUpdate)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("password")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -206,14 +197,14 @@ impl Component for ResetPasswordStep2Form {
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-sm-10">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="confirm_password"
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    autocomplete="new-password"
 | 
			
		||||
                    input_type="password"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::FormUpdate) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::FormUpdate)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("confirm_password")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -223,8 +214,8 @@ impl Component for ResetPasswordStep2Form {
 | 
			
		||||
                <button
 | 
			
		||||
                  class="btn btn-primary col-sm-1 col-form-label"
 | 
			
		||||
                  type="submit"
 | 
			
		||||
                  disabled=self.common.is_task_running()
 | 
			
		||||
                  onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})>
 | 
			
		||||
                  disabled={self.common.is_task_running()}
 | 
			
		||||
                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::Submit})}>
 | 
			
		||||
                  {"Submit"}
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
@ -1,34 +1,30 @@
 | 
			
		||||
use yew_router::{
 | 
			
		||||
    components::{RouterAnchor, RouterButton},
 | 
			
		||||
    Switch,
 | 
			
		||||
};
 | 
			
		||||
use yew_router::Routable;
 | 
			
		||||
 | 
			
		||||
#[derive(Switch, Debug, Clone)]
 | 
			
		||||
#[derive(Routable, Debug, Clone, PartialEq)]
 | 
			
		||||
pub enum AppRoute {
 | 
			
		||||
    #[to = "/login"]
 | 
			
		||||
    #[at("/login")]
 | 
			
		||||
    Login,
 | 
			
		||||
    #[to = "/reset-password/step1"]
 | 
			
		||||
    #[at("/reset-password/step1")]
 | 
			
		||||
    StartResetPassword,
 | 
			
		||||
    #[to = "/reset-password/step2/{token}"]
 | 
			
		||||
    FinishResetPassword(String),
 | 
			
		||||
    #[to = "/users/create"]
 | 
			
		||||
    #[at("/reset-password/step2/:token")]
 | 
			
		||||
    FinishResetPassword { token: String },
 | 
			
		||||
    #[at("/users/create")]
 | 
			
		||||
    CreateUser,
 | 
			
		||||
    #[to = "/users"]
 | 
			
		||||
    #[at("/users")]
 | 
			
		||||
    ListUsers,
 | 
			
		||||
    #[to = "/user/{user_id}/password"]
 | 
			
		||||
    ChangePassword(String),
 | 
			
		||||
    #[to = "/user/{user_id}"]
 | 
			
		||||
    UserDetails(String),
 | 
			
		||||
    #[to = "/groups/create"]
 | 
			
		||||
    #[at("/user/:user_id/password")]
 | 
			
		||||
    ChangePassword { user_id: String },
 | 
			
		||||
    #[at("/user/:user_id")]
 | 
			
		||||
    UserDetails { user_id: String },
 | 
			
		||||
    #[at("/groups/create")]
 | 
			
		||||
    CreateGroup,
 | 
			
		||||
    #[to = "/groups"]
 | 
			
		||||
    #[at("/groups")]
 | 
			
		||||
    ListGroups,
 | 
			
		||||
    #[to = "/group/{group_id}"]
 | 
			
		||||
    GroupDetails(i64),
 | 
			
		||||
    #[to = "/"]
 | 
			
		||||
    #[at("/group/:group_id")]
 | 
			
		||||
    GroupDetails { group_id: i64 },
 | 
			
		||||
    #[at("/")]
 | 
			
		||||
    Index,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub type Link = RouterAnchor<AppRoute>;
 | 
			
		||||
 | 
			
		||||
pub type NavButton = RouterButton<AppRoute>;
 | 
			
		||||
pub type Link = yew_router::components::Link<AppRoute>;
 | 
			
		||||
pub type Redirect = yew_router::components::Redirect<AppRoute>;
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,6 @@
 | 
			
		||||
use yew::{html::ChangeData, prelude::*};
 | 
			
		||||
use yewtil::NeqAssign;
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
 | 
			
		||||
pub struct Select {
 | 
			
		||||
    link: ComponentLink<Self>,
 | 
			
		||||
    props: SelectProps,
 | 
			
		||||
    node_ref: NodeRef,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,100 +11,70 @@ pub struct SelectProps {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum SelectMsg {
 | 
			
		||||
    OnSelectChange(ChangeData),
 | 
			
		||||
    OnSelectChange,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Select {
 | 
			
		||||
    fn get_nth_child_props(&self, nth: i32) -> Option<SelectOptionProps> {
 | 
			
		||||
    fn get_nth_child_props(&self, ctx: &Context<Self>, nth: i32) -> Option<SelectOptionProps> {
 | 
			
		||||
        if nth == -1 {
 | 
			
		||||
            return None;
 | 
			
		||||
        }
 | 
			
		||||
        self.props
 | 
			
		||||
        ctx.props()
 | 
			
		||||
            .children
 | 
			
		||||
            .iter()
 | 
			
		||||
            .nth(nth as usize)
 | 
			
		||||
            .map(|child| child.props)
 | 
			
		||||
            .map(|child| (*child.props).clone())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn send_selection_update(&self) {
 | 
			
		||||
    fn send_selection_update(&self, ctx: &Context<Self>) {
 | 
			
		||||
        let select_node = self.node_ref.cast::<web_sys::HtmlSelectElement>().unwrap();
 | 
			
		||||
        self.props
 | 
			
		||||
        ctx.props()
 | 
			
		||||
            .on_selection_change
 | 
			
		||||
            .emit(self.get_nth_child_props(select_node.selected_index()))
 | 
			
		||||
            .emit(self.get_nth_child_props(ctx, select_node.selected_index()))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Component for Select {
 | 
			
		||||
    type Message = SelectMsg;
 | 
			
		||||
    type Properties = SelectProps;
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(_: &Context<Self>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            link,
 | 
			
		||||
            props,
 | 
			
		||||
            node_ref: NodeRef::default(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn rendered(&mut self, _first_render: bool) {
 | 
			
		||||
        self.send_selection_update();
 | 
			
		||||
    fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
 | 
			
		||||
        self.send_selection_update(ctx);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        let SelectMsg::OnSelectChange(data) = msg;
 | 
			
		||||
        match data {
 | 
			
		||||
            ChangeData::Select(_) => self.send_selection_update(),
 | 
			
		||||
            _ => unreachable!(),
 | 
			
		||||
        }
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, _: Self::Message) -> bool {
 | 
			
		||||
        self.send_selection_update(ctx);
 | 
			
		||||
        false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.props.children.neq_assign(props.children)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
            <select class="form-select"
 | 
			
		||||
              ref=self.node_ref.clone()
 | 
			
		||||
              disabled=self.props.children.is_empty()
 | 
			
		||||
              onchange=self.link.callback(SelectMsg::OnSelectChange)>
 | 
			
		||||
            { self.props.children.clone() }
 | 
			
		||||
              ref={self.node_ref.clone()}
 | 
			
		||||
              disabled={ctx.props().children.is_empty()}
 | 
			
		||||
              onchange={ctx.link().callback(|_| SelectMsg::OnSelectChange)}>
 | 
			
		||||
            { ctx.props().children.clone() }
 | 
			
		||||
            </select>
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct SelectOption {
 | 
			
		||||
    props: SelectOptionProps,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(yew::Properties, Clone, PartialEq, Eq, Debug)]
 | 
			
		||||
pub struct SelectOptionProps {
 | 
			
		||||
    pub value: String,
 | 
			
		||||
    pub text: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Component for SelectOption {
 | 
			
		||||
    type Message = ();
 | 
			
		||||
    type Properties = SelectOptionProps;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
 | 
			
		||||
        Self { props }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, _: Self::Message) -> ShouldRender {
 | 
			
		||||
        false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.props.neq_assign(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
#[function_component(SelectOption)]
 | 
			
		||||
pub fn select_option(props: &SelectOptionProps) -> Html {
 | 
			
		||||
    html! {
 | 
			
		||||
          <option value=self.props.value.clone()>
 | 
			
		||||
            {&self.props.text}
 | 
			
		||||
      <option value={props.value.clone()}>
 | 
			
		||||
        {&props.text}
 | 
			
		||||
      </option>
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ use crate::{
 | 
			
		||||
    components::{
 | 
			
		||||
        add_user_to_group::AddUserToGroupComponent,
 | 
			
		||||
        remove_user_from_group::RemoveUserFromGroupComponent,
 | 
			
		||||
        router::{AppRoute, Link, NavButton},
 | 
			
		||||
        router::{AppRoute, Link},
 | 
			
		||||
        user_details_form::UserDetailsForm,
 | 
			
		||||
    },
 | 
			
		||||
    infra::common_component::{CommonComponent, CommonComponentParts},
 | 
			
		||||
@ -47,7 +47,7 @@ pub struct Props {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<UserDetails> for UserDetails {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::UserDetailsResponse(response) => match response {
 | 
			
		||||
                Ok(user) => self.user = Some(user.user),
 | 
			
		||||
@ -77,10 +77,11 @@ impl CommonComponent<UserDetails> for UserDetails {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UserDetails {
 | 
			
		||||
    fn get_user_details(&mut self) {
 | 
			
		||||
    fn get_user_details(&mut self, ctx: &Context<Self>) {
 | 
			
		||||
        self.common.call_graphql::<GetUserDetails, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            get_user_details::Variables {
 | 
			
		||||
                id: self.common.username.clone(),
 | 
			
		||||
                id: ctx.props().username.clone(),
 | 
			
		||||
            },
 | 
			
		||||
            Msg::UserDetailsResponse,
 | 
			
		||||
            "Error trying to fetch user details",
 | 
			
		||||
@ -99,24 +100,25 @@ impl UserDetails {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_group_memberships(&self, u: &User) -> Html {
 | 
			
		||||
    fn view_group_memberships(&self, ctx: &Context<Self>, u: &User) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        let make_group_row = |group: &Group| {
 | 
			
		||||
            let display_name = group.display_name.clone();
 | 
			
		||||
            html! {
 | 
			
		||||
              <tr key="groupRow_".to_string() + &display_name>
 | 
			
		||||
                {if self.common.is_admin { html! {
 | 
			
		||||
              <tr key={"groupRow_".to_string() + &display_name}>
 | 
			
		||||
                {if ctx.props().is_admin { html! {
 | 
			
		||||
                  <>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <Link route=AppRoute::GroupDetails(group.id)>
 | 
			
		||||
                      <Link to={AppRoute::GroupDetails{group_id: group.id}}>
 | 
			
		||||
                        {&group.display_name}
 | 
			
		||||
                      </Link>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                      <RemoveUserFromGroupComponent
 | 
			
		||||
                        username=u.id.clone()
 | 
			
		||||
                        group_id=group.id
 | 
			
		||||
                        on_user_removed_from_group=self.common.callback(Msg::OnUserRemovedFromGroup)
 | 
			
		||||
                        on_error=self.common.callback(Msg::OnError)/>
 | 
			
		||||
                        username={u.id.clone()}
 | 
			
		||||
                        group_id={group.id}
 | 
			
		||||
                        on_user_removed_from_group={link.callback(Msg::OnUserRemovedFromGroup)}
 | 
			
		||||
                        on_error={link.callback(Msg::OnError)}/>
 | 
			
		||||
                    </td>
 | 
			
		||||
                  </>
 | 
			
		||||
                } } else { html! {
 | 
			
		||||
@ -133,7 +135,7 @@ impl UserDetails {
 | 
			
		||||
                <thead>
 | 
			
		||||
                  <tr key="headerRow">
 | 
			
		||||
                    <th>{"Group"}</th>
 | 
			
		||||
                    { if self.common.is_admin { html!{ <th></th> }} else { html!{} }}
 | 
			
		||||
                    { if ctx.props().is_admin { html!{ <th></th> }} else { html!{} }}
 | 
			
		||||
                  </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody>
 | 
			
		||||
@ -153,14 +155,15 @@ impl UserDetails {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_add_group_button(&self, u: &User) -> Html {
 | 
			
		||||
        if self.common.is_admin {
 | 
			
		||||
    fn view_add_group_button(&self, ctx: &Context<Self>, u: &User) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        if ctx.props().is_admin {
 | 
			
		||||
            html! {
 | 
			
		||||
                <AddUserToGroupComponent
 | 
			
		||||
                    username=u.id.clone()
 | 
			
		||||
                    groups=u.groups.clone()
 | 
			
		||||
                    on_error=self.common.callback(Msg::OnError)
 | 
			
		||||
                    on_user_added_to_group=self.common.callback(Msg::OnUserAddedToGroup)/>
 | 
			
		||||
                    username={u.id.clone()}
 | 
			
		||||
                    groups={u.groups.clone()}
 | 
			
		||||
                    on_error={link.callback(Msg::OnError)}
 | 
			
		||||
                    on_user_added_to_group={link.callback(Msg::OnUserAddedToGroup)}/>
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            html! {}
 | 
			
		||||
@ -172,24 +175,20 @@ impl Component for UserDetails {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let mut table = Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            user: None,
 | 
			
		||||
        };
 | 
			
		||||
        table.get_user_details();
 | 
			
		||||
        table.get_user_details(ctx);
 | 
			
		||||
        table
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        match (&self.user, &self.common.error) {
 | 
			
		||||
            (None, None) => html! {{"Loading..."}},
 | 
			
		||||
            (None, Some(e)) => html! {<div>{"Error: "}{e.to_string()}</div>},
 | 
			
		||||
@ -198,20 +197,19 @@ impl Component for UserDetails {
 | 
			
		||||
                  <>
 | 
			
		||||
                    <h3>{u.id.to_string()}</h3>
 | 
			
		||||
                    <div class="d-flex flex-row-reverse">
 | 
			
		||||
                      <NavButton
 | 
			
		||||
                        route=AppRoute::ChangePassword(u.id.clone())
 | 
			
		||||
                      <Link
 | 
			
		||||
                        to={AppRoute::ChangePassword{user_id: u.id.clone()}}
 | 
			
		||||
                        classes="btn btn-secondary">
 | 
			
		||||
                        <i class="bi-key me-2"></i>
 | 
			
		||||
                        {"Modify password"}
 | 
			
		||||
                      </NavButton>
 | 
			
		||||
                      </Link>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <h5 class="row m-3 fw-bold">{"User details"}</h5>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <UserDetailsForm
 | 
			
		||||
                      user=u.clone() />
 | 
			
		||||
                    {self.view_group_memberships(u)}
 | 
			
		||||
                    {self.view_add_group_button(u)}
 | 
			
		||||
                    <UserDetailsForm user={u.clone()} />
 | 
			
		||||
                    {self.view_group_memberships(ctx, u)}
 | 
			
		||||
                    {self.view_add_group_button(ctx, u)}
 | 
			
		||||
                    {self.view_messages(error)}
 | 
			
		||||
                  </>
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -5,15 +5,19 @@ use crate::{
 | 
			
		||||
    infra::common_component::{CommonComponent, CommonComponentParts},
 | 
			
		||||
};
 | 
			
		||||
use anyhow::{bail, Error, Result};
 | 
			
		||||
use gloo_file::{
 | 
			
		||||
    callbacks::{read_as_bytes, FileReader},
 | 
			
		||||
    File,
 | 
			
		||||
};
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use validator_derive::Validate;
 | 
			
		||||
use wasm_bindgen::JsCast;
 | 
			
		||||
use yew::{prelude::*, services::ConsoleService};
 | 
			
		||||
use web_sys::{FileList, HtmlInputElement, InputEvent};
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
use yew_form_derive::Model;
 | 
			
		||||
 | 
			
		||||
#[derive(PartialEq, Eq, Clone, Default)]
 | 
			
		||||
#[derive(Default)]
 | 
			
		||||
struct JsFile {
 | 
			
		||||
    file: Option<web_sys::File>,
 | 
			
		||||
    file: Option<File>,
 | 
			
		||||
    contents: Option<Vec<u8>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,7 +25,7 @@ impl ToString for JsFile {
 | 
			
		||||
    fn to_string(&self) -> String {
 | 
			
		||||
        self.file
 | 
			
		||||
            .as_ref()
 | 
			
		||||
            .map(web_sys::File::name)
 | 
			
		||||
            .map(File::name)
 | 
			
		||||
            .unwrap_or_else(String::new)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -64,17 +68,21 @@ pub struct UserDetailsForm {
 | 
			
		||||
    common: CommonComponentParts<Self>,
 | 
			
		||||
    form: yew_form::Form<UserModel>,
 | 
			
		||||
    avatar: JsFile,
 | 
			
		||||
    reader: Option<FileReader>,
 | 
			
		||||
    /// True if we just successfully updated the user, to display a success message.
 | 
			
		||||
    just_updated: bool,
 | 
			
		||||
    user: User,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub enum Msg {
 | 
			
		||||
    /// A form field changed.
 | 
			
		||||
    Update,
 | 
			
		||||
    /// A new file was selected.
 | 
			
		||||
    FileSelected(File),
 | 
			
		||||
    /// The "Submit" button was clicked.
 | 
			
		||||
    SubmitClicked,
 | 
			
		||||
    /// A picked file finished loading.
 | 
			
		||||
    FileLoaded(yew::services::reader::FileData),
 | 
			
		||||
    FileLoaded(String, Result<Vec<u8>>),
 | 
			
		||||
    /// We got the response from the server about our update message.
 | 
			
		||||
    UserUpdated(Result<update_user::ResponseData>),
 | 
			
		||||
}
 | 
			
		||||
@ -86,53 +94,47 @@ pub struct Props {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<UserDetailsForm> for UserDetailsForm {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::Update => {
 | 
			
		||||
                let window = web_sys::window().expect("no global `window` exists");
 | 
			
		||||
                let document = window.document().expect("should have a document on window");
 | 
			
		||||
                let input = document
 | 
			
		||||
                    .get_element_by_id("avatarInput")
 | 
			
		||||
                    .expect("Form field avatarInput should be present")
 | 
			
		||||
                    .dyn_into::<web_sys::HtmlInputElement>()
 | 
			
		||||
                    .expect("Should be an HtmlInputElement");
 | 
			
		||||
                ConsoleService::log("Form update");
 | 
			
		||||
                if let Some(files) = input.files() {
 | 
			
		||||
                    ConsoleService::log("Got file list");
 | 
			
		||||
                    if files.length() > 0 {
 | 
			
		||||
                        ConsoleService::log("Got a file");
 | 
			
		||||
                        let new_avatar = JsFile {
 | 
			
		||||
                            file: files.item(0),
 | 
			
		||||
            Msg::Update => Ok(true),
 | 
			
		||||
            Msg::FileSelected(new_avatar) => {
 | 
			
		||||
                if self.avatar.file.as_ref().map(|f| f.name()) != Some(new_avatar.name()) {
 | 
			
		||||
                    let file_name = new_avatar.name();
 | 
			
		||||
                    let link = ctx.link().clone();
 | 
			
		||||
                    self.reader = Some(read_as_bytes(&new_avatar, move |res| {
 | 
			
		||||
                        link.send_message(Msg::FileLoaded(
 | 
			
		||||
                            file_name,
 | 
			
		||||
                            res.map_err(|e| anyhow::anyhow!("{:#}", e)),
 | 
			
		||||
                        ))
 | 
			
		||||
                    }));
 | 
			
		||||
                    self.avatar = JsFile {
 | 
			
		||||
                        file: Some(new_avatar),
 | 
			
		||||
                        contents: None,
 | 
			
		||||
                    };
 | 
			
		||||
                        if self.avatar.file.as_ref().map(|f| f.name())
 | 
			
		||||
                            != new_avatar.file.as_ref().map(|f| f.name())
 | 
			
		||||
                        {
 | 
			
		||||
                            if let Some(ref file) = new_avatar.file {
 | 
			
		||||
                                self.mut_common().read_file(file.clone(), Msg::FileLoaded)?;
 | 
			
		||||
                            }
 | 
			
		||||
                            self.avatar = new_avatar;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                Ok(true)
 | 
			
		||||
            }
 | 
			
		||||
            Msg::SubmitClicked => self.submit_user_update_form(),
 | 
			
		||||
            Msg::SubmitClicked => self.submit_user_update_form(ctx),
 | 
			
		||||
            Msg::UserUpdated(response) => self.user_update_finished(response),
 | 
			
		||||
            Msg::FileLoaded(data) => {
 | 
			
		||||
                self.common.cancel_task();
 | 
			
		||||
            Msg::FileLoaded(file_name, data) => {
 | 
			
		||||
                if let Some(file) = &self.avatar.file {
 | 
			
		||||
                    if file.name() == data.name {
 | 
			
		||||
                        if !is_valid_jpeg(data.content.as_slice()) {
 | 
			
		||||
                    if file.name() == file_name {
 | 
			
		||||
                        let data = data?;
 | 
			
		||||
                        if !is_valid_jpeg(data.as_slice()) {
 | 
			
		||||
                            // Clear the selection.
 | 
			
		||||
                            self.avatar = JsFile::default();
 | 
			
		||||
                            bail!("Chosen image is not a valid JPEG");
 | 
			
		||||
                        } else {
 | 
			
		||||
                            self.avatar.contents = Some(data.content);
 | 
			
		||||
                            self.avatar.contents = Some(data);
 | 
			
		||||
                            return Ok(true);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                self.reader = None;
 | 
			
		||||
                Ok(false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@ -147,37 +149,36 @@ impl Component for UserDetailsForm {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = Props;
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let model = UserModel {
 | 
			
		||||
            email: props.user.email.clone(),
 | 
			
		||||
            display_name: props.user.display_name.clone(),
 | 
			
		||||
            first_name: props.user.first_name.clone(),
 | 
			
		||||
            last_name: props.user.last_name.clone(),
 | 
			
		||||
            email: ctx.props().user.email.clone(),
 | 
			
		||||
            display_name: ctx.props().user.display_name.clone(),
 | 
			
		||||
            first_name: ctx.props().user.first_name.clone(),
 | 
			
		||||
            last_name: ctx.props().user.last_name.clone(),
 | 
			
		||||
        };
 | 
			
		||||
        Self {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            form: yew_form::Form::new(model),
 | 
			
		||||
            avatar: JsFile::default(),
 | 
			
		||||
            just_updated: false,
 | 
			
		||||
            reader: None,
 | 
			
		||||
            user: ctx.props().user.clone(),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        self.just_updated = false;
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        type Field = yew_form::Field<UserModel>;
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
 | 
			
		||||
        let avatar_base64 = maybe_to_base64(&self.avatar).unwrap_or_default();
 | 
			
		||||
        let avatar_string = avatar_base64
 | 
			
		||||
            .as_deref()
 | 
			
		||||
            .or(self.common.user.avatar.as_deref())
 | 
			
		||||
            .or(self.user.avatar.as_deref())
 | 
			
		||||
            .unwrap_or("");
 | 
			
		||||
        html! {
 | 
			
		||||
          <div class="py-3">
 | 
			
		||||
@ -188,7 +189,7 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                  {"User ID: "}
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <span id="userId" class="form-control-static"><i>{&self.common.user.id}</i></span>
 | 
			
		||||
                  <span id="userId" class="form-control-static"><i>{&self.user.id}</i></span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="form-group row mb-3">
 | 
			
		||||
@ -197,7 +198,7 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                  {"Creation date: "}
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <span id="creationDate" class="form-control-static">{&self.common.user.creation_date.naive_local().date()}</span>
 | 
			
		||||
                  <span id="creationDate" class="form-control-static">{&self.user.creation_date.naive_local().date()}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="form-group row mb-3">
 | 
			
		||||
@ -206,7 +207,7 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                  {"UUID: "}
 | 
			
		||||
                </label>
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <span id="creationDate" class="form-control-static">{&self.common.user.uuid}</span>
 | 
			
		||||
                  <span id="creationDate" class="form-control-static">{&self.user.uuid}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="form-group row mb-3">
 | 
			
		||||
@ -221,10 +222,10 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="email"
 | 
			
		||||
                    autocomplete="email"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("email")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -240,10 +241,10 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    class_invalid="is-invalid has-error"
 | 
			
		||||
                    class_valid="has-success"
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="display_name"
 | 
			
		||||
                    autocomplete="name"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("display_name")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -257,10 +258,10 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="first_name"
 | 
			
		||||
                    autocomplete="given-name"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("first_name")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -274,10 +275,10 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                <div class="col-8">
 | 
			
		||||
                  <Field
 | 
			
		||||
                    class="form-control"
 | 
			
		||||
                    form=&self.form
 | 
			
		||||
                    form={&self.form}
 | 
			
		||||
                    field_name="last_name"
 | 
			
		||||
                    autocomplete="family-name"
 | 
			
		||||
                    oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                    oninput={link.callback(|_| Msg::Update)} />
 | 
			
		||||
                  <div class="invalid-feedback">
 | 
			
		||||
                    {&self.form.field_message("last_name")}
 | 
			
		||||
                  </div>
 | 
			
		||||
@ -296,7 +297,10 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                        id="avatarInput"
 | 
			
		||||
                        type="file"
 | 
			
		||||
                        accept="image/jpeg"
 | 
			
		||||
                        oninput=self.common.callback(|_| Msg::Update) />
 | 
			
		||||
                        oninput={link.callback(|e: InputEvent| {
 | 
			
		||||
                            let input: HtmlInputElement = e.target_unchecked_into();
 | 
			
		||||
                            Self::upload_files(input.files())
 | 
			
		||||
                        })} />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="col-4">
 | 
			
		||||
                      <img
 | 
			
		||||
@ -312,8 +316,8 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                <button
 | 
			
		||||
                  type="submit"
 | 
			
		||||
                  class="btn btn-primary col-auto col-form-label"
 | 
			
		||||
                  disabled=self.common.is_task_running()
 | 
			
		||||
                  onclick=self.common.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})>
 | 
			
		||||
                  disabled={self.common.is_task_running()}
 | 
			
		||||
                  onclick={link.callback(|e: MouseEvent| {e.prevent_default(); Msg::SubmitClicked})}>
 | 
			
		||||
                  <i class="bi-save me-2"></i>
 | 
			
		||||
                  {"Save changes"}
 | 
			
		||||
                </button>
 | 
			
		||||
@ -328,7 +332,7 @@ impl Component for UserDetailsForm {
 | 
			
		||||
                }
 | 
			
		||||
              } else { html! {} }
 | 
			
		||||
            }
 | 
			
		||||
            <div hidden=!self.just_updated>
 | 
			
		||||
            <div hidden={!self.just_updated}>
 | 
			
		||||
              <div class="alert alert-success mt-4">{"User successfully updated!"}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
@ -337,12 +341,10 @@ impl Component for UserDetailsForm {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UserDetailsForm {
 | 
			
		||||
    fn submit_user_update_form(&mut self) -> Result<bool> {
 | 
			
		||||
        ConsoleService::log("Submit");
 | 
			
		||||
    fn submit_user_update_form(&mut self, ctx: &Context<Self>) -> Result<bool> {
 | 
			
		||||
        if !self.form.validate() {
 | 
			
		||||
            bail!("Invalid inputs");
 | 
			
		||||
        }
 | 
			
		||||
        ConsoleService::log("Valid inputs");
 | 
			
		||||
        if let JsFile {
 | 
			
		||||
            file: Some(_),
 | 
			
		||||
            contents: None,
 | 
			
		||||
@ -350,10 +352,9 @@ impl UserDetailsForm {
 | 
			
		||||
        {
 | 
			
		||||
            bail!("Image file hasn't finished loading, try again");
 | 
			
		||||
        }
 | 
			
		||||
        ConsoleService::log("File is correctly loaded");
 | 
			
		||||
        let base_user = &self.common.user;
 | 
			
		||||
        let base_user = &self.user;
 | 
			
		||||
        let mut user_input = update_user::UpdateUserInput {
 | 
			
		||||
            id: self.common.user.id.clone(),
 | 
			
		||||
            id: self.user.id.clone(),
 | 
			
		||||
            email: None,
 | 
			
		||||
            displayName: None,
 | 
			
		||||
            firstName: None,
 | 
			
		||||
@ -378,12 +379,11 @@ impl UserDetailsForm {
 | 
			
		||||
        user_input.avatar = maybe_to_base64(&self.avatar)?;
 | 
			
		||||
        // Nothing changed.
 | 
			
		||||
        if user_input == default_user_input {
 | 
			
		||||
            ConsoleService::log("No changes");
 | 
			
		||||
            return Ok(false);
 | 
			
		||||
        }
 | 
			
		||||
        let req = update_user::Variables { user: user_input };
 | 
			
		||||
        ConsoleService::log("Querying");
 | 
			
		||||
        self.common.call_graphql::<UpdateUser, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            req,
 | 
			
		||||
            Msg::UserUpdated,
 | 
			
		||||
            "Error trying to update user",
 | 
			
		||||
@ -392,23 +392,30 @@ impl UserDetailsForm {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn user_update_finished(&mut self, r: Result<update_user::ResponseData>) -> Result<bool> {
 | 
			
		||||
        self.common.cancel_task();
 | 
			
		||||
        match r {
 | 
			
		||||
            Err(e) => return Err(e),
 | 
			
		||||
            Ok(_) => {
 | 
			
		||||
        r?;
 | 
			
		||||
        let model = self.form.model();
 | 
			
		||||
                self.common.user.email = model.email;
 | 
			
		||||
                self.common.user.display_name = model.display_name;
 | 
			
		||||
                self.common.user.first_name = model.first_name;
 | 
			
		||||
                self.common.user.last_name = model.last_name;
 | 
			
		||||
        self.user.email = model.email;
 | 
			
		||||
        self.user.display_name = model.display_name;
 | 
			
		||||
        self.user.first_name = model.first_name;
 | 
			
		||||
        self.user.last_name = model.last_name;
 | 
			
		||||
        if let Some(avatar) = maybe_to_base64(&self.avatar)? {
 | 
			
		||||
                    self.common.user.avatar = Some(avatar);
 | 
			
		||||
            self.user.avatar = Some(avatar);
 | 
			
		||||
        }
 | 
			
		||||
        self.just_updated = true;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        Ok(true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn upload_files(files: Option<FileList>) -> Msg {
 | 
			
		||||
        if let Some(files) = files {
 | 
			
		||||
            if files.length() > 0 {
 | 
			
		||||
                Msg::FileSelected(File::from(files.item(0).unwrap()))
 | 
			
		||||
            } else {
 | 
			
		||||
                Msg::Update
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            Msg::Update
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn is_valid_jpeg(bytes: &[u8]) -> bool {
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ pub enum Msg {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl CommonComponent<UserTable> for UserTable {
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
    fn handle_msg(&mut self, _: &Context<Self>, msg: <Self as Component>::Message) -> Result<bool> {
 | 
			
		||||
        match msg {
 | 
			
		||||
            Msg::ListUsersResponse(users) => {
 | 
			
		||||
                self.users = Some(users?.users.into_iter().collect());
 | 
			
		||||
@ -55,8 +55,9 @@ impl CommonComponent<UserTable> for UserTable {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UserTable {
 | 
			
		||||
    fn get_users(&mut self, req: Option<RequestFilter>) {
 | 
			
		||||
    fn get_users(&mut self, ctx: &Context<Self>, req: Option<RequestFilter>) {
 | 
			
		||||
        self.common.call_graphql::<ListUsersQuery, _>(
 | 
			
		||||
            ctx,
 | 
			
		||||
            list_users_query::Variables { filters: req },
 | 
			
		||||
            Msg::ListUsersResponse,
 | 
			
		||||
            "Error trying to fetch users",
 | 
			
		||||
@ -68,27 +69,23 @@ impl Component for UserTable {
 | 
			
		||||
    type Message = Msg;
 | 
			
		||||
    type Properties = ();
 | 
			
		||||
 | 
			
		||||
    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
 | 
			
		||||
    fn create(ctx: &Context<Self>) -> Self {
 | 
			
		||||
        let mut table = UserTable {
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(props, link),
 | 
			
		||||
            common: CommonComponentParts::<Self>::create(),
 | 
			
		||||
            users: None,
 | 
			
		||||
        };
 | 
			
		||||
        table.get_users(None);
 | 
			
		||||
        table.get_users(ctx, None);
 | 
			
		||||
        table
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, msg)
 | 
			
		||||
    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
 | 
			
		||||
        CommonComponentParts::<Self>::update(self, ctx, msg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn change(&mut self, props: Self::Properties) -> ShouldRender {
 | 
			
		||||
        self.common.change(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view(&self) -> Html {
 | 
			
		||||
    fn view(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        html! {
 | 
			
		||||
            <div>
 | 
			
		||||
              {self.view_users()}
 | 
			
		||||
              {self.view_users(ctx)}
 | 
			
		||||
              {self.view_errors()}
 | 
			
		||||
            </div>
 | 
			
		||||
        }
 | 
			
		||||
@ -96,7 +93,7 @@ impl Component for UserTable {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl UserTable {
 | 
			
		||||
    fn view_users(&self) -> Html {
 | 
			
		||||
    fn view_users(&self, ctx: &Context<Self>) -> Html {
 | 
			
		||||
        let make_table = |users: &Vec<User>| {
 | 
			
		||||
            html! {
 | 
			
		||||
                <div class="table-responsive">
 | 
			
		||||
@ -113,7 +110,7 @@ impl UserTable {
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                      {users.iter().map(|u| self.view_user(u)).collect::<Vec<_>>()}
 | 
			
		||||
                      {users.iter().map(|u| self.view_user(ctx, u)).collect::<Vec<_>>()}
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                  </table>
 | 
			
		||||
                </div>
 | 
			
		||||
@ -125,10 +122,11 @@ impl UserTable {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn view_user(&self, user: &User) -> Html {
 | 
			
		||||
    fn view_user(&self, ctx: &Context<Self>, user: &User) -> Html {
 | 
			
		||||
        let link = &ctx.link();
 | 
			
		||||
        html! {
 | 
			
		||||
          <tr key=user.id.clone()>
 | 
			
		||||
              <td><Link route=AppRoute::UserDetails(user.id.clone())>{&user.id}</Link></td>
 | 
			
		||||
          <tr key={user.id.clone()}>
 | 
			
		||||
              <td><Link to={AppRoute::UserDetails{user_id: user.id.clone()}}>{&user.id}</Link></td>
 | 
			
		||||
              <td>{&user.email}</td>
 | 
			
		||||
              <td>{&user.display_name}</td>
 | 
			
		||||
              <td>{&user.first_name}</td>
 | 
			
		||||
@ -136,9 +134,9 @@ impl UserTable {
 | 
			
		||||
              <td>{&user.creation_date.naive_local().date()}</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <DeleteUser
 | 
			
		||||
                  username=user.id.clone()
 | 
			
		||||
                  on_user_deleted=self.common.callback(Msg::OnUserDeleted)
 | 
			
		||||
                  on_error=self.common.callback(Msg::OnError)/>
 | 
			
		||||
                  username={user.id.clone()}
 | 
			
		||||
                  on_user_deleted={link.callback(Msg::OnUserDeleted)}
 | 
			
		||||
                  on_error={link.callback(Msg::OnError)}/>
 | 
			
		||||
              </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,138 +1,84 @@
 | 
			
		||||
use super::cookies::set_cookie;
 | 
			
		||||
use anyhow::{anyhow, Context, Result};
 | 
			
		||||
use gloo_net::http::{Method, Request};
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use lldap_auth::{login, registration, JWTClaims};
 | 
			
		||||
 | 
			
		||||
use yew::{
 | 
			
		||||
    callback::Callback,
 | 
			
		||||
    format::Json,
 | 
			
		||||
    services::fetch::{Credentials, FetchOptions, FetchService, FetchTask, Request, Response},
 | 
			
		||||
};
 | 
			
		||||
use serde::{de::DeserializeOwned, Serialize};
 | 
			
		||||
use web_sys::RequestCredentials;
 | 
			
		||||
 | 
			
		||||
#[derive(Default)]
 | 
			
		||||
pub struct HostService {}
 | 
			
		||||
 | 
			
		||||
fn get_default_options() -> FetchOptions {
 | 
			
		||||
    FetchOptions {
 | 
			
		||||
        credentials: Some(Credentials::SameOrigin),
 | 
			
		||||
        ..FetchOptions::default()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn get_claims_from_jwt(jwt: &str) -> Result<JWTClaims> {
 | 
			
		||||
    use jwt::*;
 | 
			
		||||
    let token = Token::<header::Header, JWTClaims, token::Unverified>::parse_unverified(jwt)?;
 | 
			
		||||
    Ok(token.claims().clone())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn create_handler<Resp, CallbackResult, F>(
 | 
			
		||||
    callback: Callback<Result<CallbackResult>>,
 | 
			
		||||
    handler: F,
 | 
			
		||||
) -> Callback<Response<Result<Resp>>>
 | 
			
		||||
where
 | 
			
		||||
    F: Fn(http::StatusCode, Resp) -> Result<CallbackResult> + 'static,
 | 
			
		||||
    CallbackResult: 'static,
 | 
			
		||||
{
 | 
			
		||||
    Callback::once(move |response: Response<Result<Resp>>| {
 | 
			
		||||
        let (meta, maybe_data) = response.into_parts();
 | 
			
		||||
        let message = maybe_data
 | 
			
		||||
            .context("Could not reach server")
 | 
			
		||||
            .and_then(|data| handler(meta.status, data));
 | 
			
		||||
        callback.emit(message)
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
const NO_BODY: Option<()> = None;
 | 
			
		||||
 | 
			
		||||
struct RequestBody<T>(T);
 | 
			
		||||
 | 
			
		||||
impl<'a, R> From<&'a R> for RequestBody<Json<&'a R>>
 | 
			
		||||
where
 | 
			
		||||
    R: serde::ser::Serialize,
 | 
			
		||||
{
 | 
			
		||||
    fn from(request: &'a R) -> Self {
 | 
			
		||||
        Self(Json(request))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<yew::format::Nothing> for RequestBody<yew::format::Nothing> {
 | 
			
		||||
    fn from(request: yew::format::Nothing) -> Self {
 | 
			
		||||
        Self(request)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn call_server<Req, CallbackResult, F, RB>(
 | 
			
		||||
async fn call_server(
 | 
			
		||||
    url: &str,
 | 
			
		||||
    request: RB,
 | 
			
		||||
    callback: Callback<Result<CallbackResult>>,
 | 
			
		||||
    body: Option<impl Serialize>,
 | 
			
		||||
    error_message: &'static str,
 | 
			
		||||
    parse_response: F,
 | 
			
		||||
) -> Result<FetchTask>
 | 
			
		||||
where
 | 
			
		||||
    F: Fn(String) -> Result<CallbackResult> + 'static,
 | 
			
		||||
    CallbackResult: 'static,
 | 
			
		||||
    RB: Into<RequestBody<Req>>,
 | 
			
		||||
    Req: Into<yew::format::Text>,
 | 
			
		||||
{
 | 
			
		||||
    let request = {
 | 
			
		||||
        // If the request type is empty (if the size is 0), it's a get.
 | 
			
		||||
        if std::mem::size_of::<RB>() == 0 {
 | 
			
		||||
            Request::get(url)
 | 
			
		||||
        } else {
 | 
			
		||||
            Request::post(url)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
) -> Result<String> {
 | 
			
		||||
    let mut request = Request::new(url)
 | 
			
		||||
        .header("Content-Type", "application/json")
 | 
			
		||||
    .body(request.into().0)?;
 | 
			
		||||
    let handler = create_handler(callback, move |status: http::StatusCode, data: String| {
 | 
			
		||||
        if status.is_success() {
 | 
			
		||||
            parse_response(data)
 | 
			
		||||
        } else {
 | 
			
		||||
            Err(anyhow!("{}[{}]: {}", error_message, status, data))
 | 
			
		||||
        .credentials(RequestCredentials::SameOrigin);
 | 
			
		||||
    if let Some(b) = body {
 | 
			
		||||
        request = request
 | 
			
		||||
            .body(serde_json::to_string(&b)?)
 | 
			
		||||
            .method(Method::POST);
 | 
			
		||||
    }
 | 
			
		||||
    });
 | 
			
		||||
    FetchService::fetch_with_options(request, get_default_options(), handler)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn call_server_json_with_error_message<CallbackResult, RB, Req>(
 | 
			
		||||
    url: &str,
 | 
			
		||||
    request: RB,
 | 
			
		||||
    callback: Callback<Result<CallbackResult>>,
 | 
			
		||||
    error_message: &'static str,
 | 
			
		||||
) -> Result<FetchTask>
 | 
			
		||||
where
 | 
			
		||||
    CallbackResult: serde::de::DeserializeOwned + 'static,
 | 
			
		||||
    RB: Into<RequestBody<Req>>,
 | 
			
		||||
    Req: Into<yew::format::Text>,
 | 
			
		||||
{
 | 
			
		||||
    call_server(url, request, callback, error_message, |data: String| {
 | 
			
		||||
        serde_json::from_str(&data).context("Could not parse response")
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn call_server_empty_response_with_error_message<RB, Req>(
 | 
			
		||||
    url: &str,
 | 
			
		||||
    request: RB,
 | 
			
		||||
    callback: Callback<Result<()>>,
 | 
			
		||||
    error_message: &'static str,
 | 
			
		||||
) -> Result<FetchTask>
 | 
			
		||||
where
 | 
			
		||||
    RB: Into<RequestBody<Req>>,
 | 
			
		||||
    Req: Into<yew::format::Text>,
 | 
			
		||||
{
 | 
			
		||||
    call_server(
 | 
			
		||||
        url,
 | 
			
		||||
        request,
 | 
			
		||||
        callback,
 | 
			
		||||
    let response = request.send().await?;
 | 
			
		||||
    if response.ok() {
 | 
			
		||||
        Ok(response.text().await?)
 | 
			
		||||
    } else {
 | 
			
		||||
        Err(anyhow!(
 | 
			
		||||
            "{}[{} {}]: {}",
 | 
			
		||||
            error_message,
 | 
			
		||||
        |_data: String| Ok(()),
 | 
			
		||||
    )
 | 
			
		||||
            response.status(),
 | 
			
		||||
            response.status_text(),
 | 
			
		||||
            response.text().await?
 | 
			
		||||
        ))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn call_server_json_with_error_message<CallbackResult, Body: Serialize>(
 | 
			
		||||
    url: &str,
 | 
			
		||||
    request: Option<Body>,
 | 
			
		||||
    error_message: &'static str,
 | 
			
		||||
) -> Result<CallbackResult>
 | 
			
		||||
where
 | 
			
		||||
    CallbackResult: DeserializeOwned + 'static,
 | 
			
		||||
{
 | 
			
		||||
    let data = call_server(url, request, error_message).await?;
 | 
			
		||||
    serde_json::from_str(&data).context("Could not parse response")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn call_server_empty_response_with_error_message<Body: Serialize>(
 | 
			
		||||
    url: &str,
 | 
			
		||||
    request: Option<Body>,
 | 
			
		||||
    error_message: &'static str,
 | 
			
		||||
) -> Result<()> {
 | 
			
		||||
    call_server(url, request, error_message).await.map(|_| ())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn set_cookies_from_jwt(response: login::ServerLoginResponse) -> Result<(String, bool)> {
 | 
			
		||||
    let jwt_claims = get_claims_from_jwt(response.token.as_str()).context("Could not parse JWT")?;
 | 
			
		||||
    let is_admin = jwt_claims.groups.contains("lldap_admin");
 | 
			
		||||
    set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
 | 
			
		||||
        .map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
 | 
			
		||||
        .map(|_| (jwt_claims.user.clone(), is_admin))
 | 
			
		||||
        .context("Error setting cookie")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl HostService {
 | 
			
		||||
    pub fn graphql_query<QueryType>(
 | 
			
		||||
    pub async fn graphql_query<QueryType>(
 | 
			
		||||
        variables: QueryType::Variables,
 | 
			
		||||
        callback: Callback<Result<QueryType::ResponseData>>,
 | 
			
		||||
        error_message: &'static str,
 | 
			
		||||
    ) -> Result<FetchTask>
 | 
			
		||||
    ) -> Result<QueryType::ResponseData>
 | 
			
		||||
    where
 | 
			
		||||
        QueryType: GraphQLQuery + 'static,
 | 
			
		||||
    {
 | 
			
		||||
@ -149,156 +95,103 @@ impl HostService {
 | 
			
		||||
                )
 | 
			
		||||
            })
 | 
			
		||||
        };
 | 
			
		||||
        let parse_graphql_response = move |data: String| {
 | 
			
		||||
            serde_json::from_str(&data)
 | 
			
		||||
                .context("Could not parse response")
 | 
			
		||||
                .and_then(unwrap_graphql_response)
 | 
			
		||||
        };
 | 
			
		||||
        let request_body = QueryType::build_query(variables);
 | 
			
		||||
        call_server(
 | 
			
		||||
        call_server_json_with_error_message::<graphql_client::Response<_>, _>(
 | 
			
		||||
            "/api/graphql",
 | 
			
		||||
            &request_body,
 | 
			
		||||
            callback,
 | 
			
		||||
            Some(request_body),
 | 
			
		||||
            error_message,
 | 
			
		||||
            parse_graphql_response,
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
        .and_then(unwrap_graphql_response)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn login_start(
 | 
			
		||||
    pub async fn login_start(
 | 
			
		||||
        request: login::ClientLoginStartRequest,
 | 
			
		||||
        callback: Callback<Result<Box<login::ServerLoginStartResponse>>>,
 | 
			
		||||
    ) -> Result<FetchTask> {
 | 
			
		||||
    ) -> Result<Box<login::ServerLoginStartResponse>> {
 | 
			
		||||
        call_server_json_with_error_message(
 | 
			
		||||
            "/auth/opaque/login/start",
 | 
			
		||||
            &request,
 | 
			
		||||
            callback,
 | 
			
		||||
            Some(request),
 | 
			
		||||
            "Could not start authentication: ",
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn login_finish(
 | 
			
		||||
        request: login::ClientLoginFinishRequest,
 | 
			
		||||
        callback: Callback<Result<(String, bool)>>,
 | 
			
		||||
    ) -> Result<FetchTask> {
 | 
			
		||||
        let set_cookies = |jwt_claims: JWTClaims| {
 | 
			
		||||
            let is_admin = jwt_claims.groups.contains("lldap_admin");
 | 
			
		||||
            set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
 | 
			
		||||
                .map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
 | 
			
		||||
                .map(|_| (jwt_claims.user.clone(), is_admin))
 | 
			
		||||
                .context("Error clearing cookie")
 | 
			
		||||
        };
 | 
			
		||||
        let parse_token = move |data: String| {
 | 
			
		||||
            serde_json::from_str::<login::ServerLoginResponse>(&data)
 | 
			
		||||
                .context("Could not parse response")
 | 
			
		||||
                .and_then(|r| {
 | 
			
		||||
                    get_claims_from_jwt(r.token.as_str())
 | 
			
		||||
                        .context("Could not parse response")
 | 
			
		||||
                        .and_then(set_cookies)
 | 
			
		||||
                })
 | 
			
		||||
        };
 | 
			
		||||
        call_server(
 | 
			
		||||
    pub async fn login_finish(request: login::ClientLoginFinishRequest) -> Result<(String, bool)> {
 | 
			
		||||
        call_server_json_with_error_message::<login::ServerLoginResponse, _>(
 | 
			
		||||
            "/auth/opaque/login/finish",
 | 
			
		||||
            &request,
 | 
			
		||||
            callback,
 | 
			
		||||
            Some(request),
 | 
			
		||||
            "Could not finish authentication",
 | 
			
		||||
            parse_token,
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
        .and_then(set_cookies_from_jwt)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn register_start(
 | 
			
		||||
    pub async fn register_start(
 | 
			
		||||
        request: registration::ClientRegistrationStartRequest,
 | 
			
		||||
        callback: Callback<Result<Box<registration::ServerRegistrationStartResponse>>>,
 | 
			
		||||
    ) -> Result<FetchTask> {
 | 
			
		||||
    ) -> Result<Box<registration::ServerRegistrationStartResponse>> {
 | 
			
		||||
        call_server_json_with_error_message(
 | 
			
		||||
            "/auth/opaque/register/start",
 | 
			
		||||
            &request,
 | 
			
		||||
            callback,
 | 
			
		||||
            Some(request),
 | 
			
		||||
            "Could not start registration: ",
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn register_finish(
 | 
			
		||||
    pub async fn register_finish(
 | 
			
		||||
        request: registration::ClientRegistrationFinishRequest,
 | 
			
		||||
        callback: Callback<Result<()>>,
 | 
			
		||||
    ) -> Result<FetchTask> {
 | 
			
		||||
    ) -> Result<()> {
 | 
			
		||||
        call_server_empty_response_with_error_message(
 | 
			
		||||
            "/auth/opaque/register/finish",
 | 
			
		||||
            &request,
 | 
			
		||||
            callback,
 | 
			
		||||
            Some(request),
 | 
			
		||||
            "Could not finish registration",
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn refresh(_request: (), callback: Callback<Result<(String, bool)>>) -> Result<FetchTask> {
 | 
			
		||||
        let set_cookies = |jwt_claims: JWTClaims| {
 | 
			
		||||
            let is_admin = jwt_claims.groups.contains("lldap_admin");
 | 
			
		||||
            set_cookie("user_id", &jwt_claims.user, &jwt_claims.exp)
 | 
			
		||||
                .map(|_| set_cookie("is_admin", &is_admin.to_string(), &jwt_claims.exp))
 | 
			
		||||
                .map(|_| (jwt_claims.user.clone(), is_admin))
 | 
			
		||||
                .context("Error clearing cookie")
 | 
			
		||||
        };
 | 
			
		||||
        let parse_token = move |data: String| {
 | 
			
		||||
            serde_json::from_str::<login::ServerLoginResponse>(&data)
 | 
			
		||||
                .context("Could not parse response")
 | 
			
		||||
                .and_then(|r| {
 | 
			
		||||
                    get_claims_from_jwt(r.token.as_str())
 | 
			
		||||
                        .context("Could not parse response")
 | 
			
		||||
                        .and_then(set_cookies)
 | 
			
		||||
                })
 | 
			
		||||
        };
 | 
			
		||||
        call_server(
 | 
			
		||||
    pub async fn refresh() -> Result<(String, bool)> {
 | 
			
		||||
        call_server_json_with_error_message::<login::ServerLoginResponse, _>(
 | 
			
		||||
            "/auth/refresh",
 | 
			
		||||
            yew::format::Nothing,
 | 
			
		||||
            callback,
 | 
			
		||||
            NO_BODY,
 | 
			
		||||
            "Could not start authentication: ",
 | 
			
		||||
            parse_token,
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
        .and_then(set_cookies_from_jwt)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The `_request` parameter is to make it the same shape as the other functions.
 | 
			
		||||
    pub fn logout(_request: (), callback: Callback<Result<()>>) -> Result<FetchTask> {
 | 
			
		||||
        call_server_empty_response_with_error_message(
 | 
			
		||||
            "/auth/logout",
 | 
			
		||||
            yew::format::Nothing,
 | 
			
		||||
            callback,
 | 
			
		||||
            "Could not logout",
 | 
			
		||||
        )
 | 
			
		||||
    pub async fn logout() -> Result<()> {
 | 
			
		||||
        call_server_empty_response_with_error_message("/auth/logout", NO_BODY, "Could not logout")
 | 
			
		||||
            .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn reset_password_step1(
 | 
			
		||||
        username: &str,
 | 
			
		||||
        callback: Callback<Result<()>>,
 | 
			
		||||
    ) -> Result<FetchTask> {
 | 
			
		||||
    pub async fn reset_password_step1(username: String) -> Result<()> {
 | 
			
		||||
        call_server_empty_response_with_error_message(
 | 
			
		||||
            &format!("/auth/reset/step1/{}", url_escape::encode_query(username)),
 | 
			
		||||
            yew::format::Nothing,
 | 
			
		||||
            callback,
 | 
			
		||||
            &format!("/auth/reset/step1/{}", url_escape::encode_query(&username)),
 | 
			
		||||
            NO_BODY,
 | 
			
		||||
            "Could not initiate password reset",
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn reset_password_step2(
 | 
			
		||||
        token: &str,
 | 
			
		||||
        callback: Callback<Result<lldap_auth::password_reset::ServerPasswordResetResponse>>,
 | 
			
		||||
    ) -> Result<FetchTask> {
 | 
			
		||||
    pub async fn reset_password_step2(
 | 
			
		||||
        token: String,
 | 
			
		||||
    ) -> Result<lldap_auth::password_reset::ServerPasswordResetResponse> {
 | 
			
		||||
        call_server_json_with_error_message(
 | 
			
		||||
            &format!("/auth/reset/step2/{}", token),
 | 
			
		||||
            yew::format::Nothing,
 | 
			
		||||
            callback,
 | 
			
		||||
            NO_BODY,
 | 
			
		||||
            "Could not validate token",
 | 
			
		||||
        )
 | 
			
		||||
        .await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn probe_password_reset(callback: Callback<Result<bool>>) -> Result<FetchTask> {
 | 
			
		||||
        let request = Request::get("/auth/reset/step1/lldap_unlikely_very_long_user_name")
 | 
			
		||||
    pub async fn probe_password_reset() -> Result<bool> {
 | 
			
		||||
        Ok(
 | 
			
		||||
            gloo_net::http::Request::get("/auth/reset/step1/lldap_unlikely_very_long_user_name")
 | 
			
		||||
                .header("Content-Type", "application/json")
 | 
			
		||||
            .body(yew::format::Nothing)?;
 | 
			
		||||
        FetchService::fetch_with_options(
 | 
			
		||||
            request,
 | 
			
		||||
            get_default_options(),
 | 
			
		||||
            create_handler(callback, move |status: http::StatusCode, _data: String| {
 | 
			
		||||
                Ok(status != http::StatusCode::NOT_FOUND)
 | 
			
		||||
            }),
 | 
			
		||||
                .send()
 | 
			
		||||
                .await?
 | 
			
		||||
                .status()
 | 
			
		||||
                != http::StatusCode::NOT_FOUND,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -21,88 +21,62 @@
 | 
			
		||||
//! [`CommonComponentParts::update`]. This will in turn call [`CommonComponent::handle_msg`] and
 | 
			
		||||
//! take care of error and task handling.
 | 
			
		||||
 | 
			
		||||
use std::{
 | 
			
		||||
    future::Future,
 | 
			
		||||
    marker::PhantomData,
 | 
			
		||||
    sync::{Arc, Mutex},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::infra::api::HostService;
 | 
			
		||||
use anyhow::{Error, Result};
 | 
			
		||||
use gloo_console::error;
 | 
			
		||||
use graphql_client::GraphQLQuery;
 | 
			
		||||
use yew::{
 | 
			
		||||
    prelude::*,
 | 
			
		||||
    services::{
 | 
			
		||||
        fetch::FetchTask,
 | 
			
		||||
        reader::{FileData, ReaderService, ReaderTask},
 | 
			
		||||
        ConsoleService,
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
use yewtil::NeqAssign;
 | 
			
		||||
use yew::prelude::*;
 | 
			
		||||
 | 
			
		||||
/// Trait required for common components.
 | 
			
		||||
pub trait CommonComponent<C: Component + CommonComponent<C>>: Component {
 | 
			
		||||
    /// Handle the incoming message. If an error is returned here, any running task will be
 | 
			
		||||
    /// cancelled, the error will be written to the [`CommonComponentParts::error`] and the
 | 
			
		||||
    /// component will be refreshed.
 | 
			
		||||
    fn handle_msg(&mut self, msg: <Self as Component>::Message) -> Result<bool>;
 | 
			
		||||
    fn handle_msg(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<Self>,
 | 
			
		||||
        msg: <Self as Component>::Message,
 | 
			
		||||
    ) -> Result<bool>;
 | 
			
		||||
    /// Get a mutable reference to the inner component parts, necessary for the CRTP.
 | 
			
		||||
    fn mut_common(&mut self) -> &mut CommonComponentParts<C>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum AnyTask {
 | 
			
		||||
    None,
 | 
			
		||||
    FetchTask(FetchTask),
 | 
			
		||||
    ReaderTask(ReaderTask),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl AnyTask {
 | 
			
		||||
    fn is_some(&self) -> bool {
 | 
			
		||||
        !matches!(self, AnyTask::None)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Option<FetchTask>> for AnyTask {
 | 
			
		||||
    fn from(task: Option<FetchTask>) -> Self {
 | 
			
		||||
        match task {
 | 
			
		||||
            Some(t) => AnyTask::FetchTask(t),
 | 
			
		||||
            None => AnyTask::None,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Structure that contains the common parts needed by most components.
 | 
			
		||||
/// The fields of [`props`] are directly accessible through a `Deref` implementation.
 | 
			
		||||
pub struct CommonComponentParts<C: CommonComponent<C>> {
 | 
			
		||||
    link: ComponentLink<C>,
 | 
			
		||||
    pub props: <C as Component>::Properties,
 | 
			
		||||
    pub error: Option<Error>,
 | 
			
		||||
    task: AnyTask,
 | 
			
		||||
    is_task_running: Arc<Mutex<bool>>,
 | 
			
		||||
    _phantom: PhantomData<C>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
			
		||||
    pub fn create() -> Self {
 | 
			
		||||
        CommonComponentParts {
 | 
			
		||||
            error: None,
 | 
			
		||||
            is_task_running: Arc::new(Mutex::new(false)),
 | 
			
		||||
            _phantom: PhantomData::<C>,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    /// Whether there is a currently running task in the background.
 | 
			
		||||
    pub fn is_task_running(&self) -> bool {
 | 
			
		||||
        self.task.is_some()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Cancel any background task.
 | 
			
		||||
    pub fn cancel_task(&mut self) {
 | 
			
		||||
        self.task = AnyTask::None;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn create(props: <C as Component>::Properties, link: ComponentLink<C>) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            link,
 | 
			
		||||
            props,
 | 
			
		||||
            error: None,
 | 
			
		||||
            task: AnyTask::None,
 | 
			
		||||
        }
 | 
			
		||||
        *self.is_task_running.lock().unwrap()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// This should be called from the [`yew::prelude::Component::update`]: it will in turn call
 | 
			
		||||
    /// [`CommonComponent::handle_msg`] and handle any resulting error.
 | 
			
		||||
    pub fn update(com: &mut C, msg: <C as Component>::Message) -> ShouldRender {
 | 
			
		||||
    pub fn update(com: &mut C, ctx: &Context<C>, msg: <C as Component>::Message) -> bool {
 | 
			
		||||
        com.mut_common().error = None;
 | 
			
		||||
        match com.handle_msg(msg) {
 | 
			
		||||
        match com.handle_msg(ctx, msg) {
 | 
			
		||||
            Err(e) => {
 | 
			
		||||
                ConsoleService::error(&e.to_string());
 | 
			
		||||
                error!(&e.to_string());
 | 
			
		||||
                com.mut_common().error = Some(e);
 | 
			
		||||
                com.mut_common().cancel_task();
 | 
			
		||||
                assert!(!*com.mut_common().is_task_running.lock().unwrap());
 | 
			
		||||
                true
 | 
			
		||||
            }
 | 
			
		||||
            Ok(b) => b,
 | 
			
		||||
@ -112,10 +86,11 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
			
		||||
    /// Same as above, but the resulting error is instead passed to the reporting function.
 | 
			
		||||
    pub fn update_and_report_error(
 | 
			
		||||
        com: &mut C,
 | 
			
		||||
        ctx: &Context<C>,
 | 
			
		||||
        msg: <C as Component>::Message,
 | 
			
		||||
        report_fn: Callback<Error>,
 | 
			
		||||
    ) -> ShouldRender {
 | 
			
		||||
        let should_render = Self::update(com, msg);
 | 
			
		||||
    ) -> bool {
 | 
			
		||||
        let should_render = Self::update(com, ctx, msg);
 | 
			
		||||
        com.mut_common()
 | 
			
		||||
            .error
 | 
			
		||||
            .take()
 | 
			
		||||
@ -126,38 +101,24 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
			
		||||
            .unwrap_or(should_render)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// This can be called from [`yew::prelude::Component::update`]: it will check if the
 | 
			
		||||
    /// properties have changed and return whether the component should update.
 | 
			
		||||
    pub fn change(&mut self, props: <C as Component>::Properties) -> ShouldRender
 | 
			
		||||
    where
 | 
			
		||||
        <C as yew::Component>::Properties: std::cmp::PartialEq,
 | 
			
		||||
    {
 | 
			
		||||
        self.props.neq_assign(props)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Create a callback from the link.
 | 
			
		||||
    pub fn callback<F, IN, M>(&self, function: F) -> Callback<IN>
 | 
			
		||||
    where
 | 
			
		||||
        M: Into<C::Message>,
 | 
			
		||||
        F: Fn(IN) -> M + 'static,
 | 
			
		||||
    {
 | 
			
		||||
        self.link.callback(function)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Call `method` from the backend with the given `request`, and pass the `callback` for the
 | 
			
		||||
    /// result. Returns whether _starting the call_ failed.
 | 
			
		||||
    pub fn call_backend<M, Req, Cb, Resp>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        method: M,
 | 
			
		||||
        req: Req,
 | 
			
		||||
        callback: Cb,
 | 
			
		||||
    ) -> Result<()>
 | 
			
		||||
    /// result.
 | 
			
		||||
    pub fn call_backend<Fut, Cb, Resp>(&mut self, ctx: &Context<C>, fut: Fut, callback: Cb)
 | 
			
		||||
    where
 | 
			
		||||
        M: Fn(Req, Callback<Resp>) -> Result<FetchTask>,
 | 
			
		||||
        Fut: Future<Output = Resp> + 'static,
 | 
			
		||||
        Cb: FnOnce(Resp) -> <C as Component>::Message + 'static,
 | 
			
		||||
    {
 | 
			
		||||
        self.task = AnyTask::FetchTask(method(req, self.link.callback_once(callback))?);
 | 
			
		||||
        Ok(())
 | 
			
		||||
        {
 | 
			
		||||
            let mut running = self.is_task_running.lock().unwrap();
 | 
			
		||||
            assert!(!*running);
 | 
			
		||||
            *running = true;
 | 
			
		||||
        }
 | 
			
		||||
        let is_task_running = self.is_task_running.clone();
 | 
			
		||||
        ctx.link().send_future(async move {
 | 
			
		||||
            let res = fut.await;
 | 
			
		||||
            *is_task_running.lock().unwrap() = false;
 | 
			
		||||
            callback(res)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Call the backend with a GraphQL query.
 | 
			
		||||
@ -165,6 +126,7 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
			
		||||
    /// `EnumCallback` should usually be left as `_`.
 | 
			
		||||
    pub fn call_graphql<QueryType, EnumCallback>(
 | 
			
		||||
        &mut self,
 | 
			
		||||
        ctx: &Context<C>,
 | 
			
		||||
        variables: QueryType::Variables,
 | 
			
		||||
        enum_callback: EnumCallback,
 | 
			
		||||
        error_message: &'static str,
 | 
			
		||||
@ -172,41 +134,10 @@ impl<C: CommonComponent<C>> CommonComponentParts<C> {
 | 
			
		||||
        QueryType: GraphQLQuery + 'static,
 | 
			
		||||
        EnumCallback: Fn(Result<QueryType::ResponseData>) -> <C as Component>::Message + 'static,
 | 
			
		||||
    {
 | 
			
		||||
        self.task = HostService::graphql_query::<QueryType>(
 | 
			
		||||
            variables,
 | 
			
		||||
            self.link.callback(enum_callback),
 | 
			
		||||
            error_message,
 | 
			
		||||
        )
 | 
			
		||||
        .map_err::<(), _>(|e| {
 | 
			
		||||
            ConsoleService::log(&e.to_string());
 | 
			
		||||
            self.error = Some(e);
 | 
			
		||||
        })
 | 
			
		||||
        .ok()
 | 
			
		||||
        .into();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub(crate) fn read_file<Cb>(&mut self, file: web_sys::File, callback: Cb) -> Result<()>
 | 
			
		||||
    where
 | 
			
		||||
        Cb: FnOnce(FileData) -> <C as Component>::Message + 'static,
 | 
			
		||||
    {
 | 
			
		||||
        self.task = AnyTask::ReaderTask(ReaderService::read_file(
 | 
			
		||||
            file,
 | 
			
		||||
            self.link.callback_once(callback),
 | 
			
		||||
        )?);
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<C: Component + CommonComponent<C>> std::ops::Deref for CommonComponentParts<C> {
 | 
			
		||||
    type Target = <C as Component>::Properties;
 | 
			
		||||
 | 
			
		||||
    fn deref(&self) -> &<Self as std::ops::Deref>::Target {
 | 
			
		||||
        &self.props
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<C: Component + CommonComponent<C>> std::ops::DerefMut for CommonComponentParts<C> {
 | 
			
		||||
    fn deref_mut(&mut self) -> &mut <Self as std::ops::Deref>::Target {
 | 
			
		||||
        &mut self.props
 | 
			
		||||
        self.call_backend(
 | 
			
		||||
            ctx,
 | 
			
		||||
            HostService::graphql_query::<QueryType>(variables, error_message),
 | 
			
		||||
            enum_callback,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
#![recursion_limit = "256"]
 | 
			
		||||
#![forbid(non_ascii_idents)]
 | 
			
		||||
#![allow(clippy::uninlined_format_args)]
 | 
			
		||||
#![allow(clippy::let_unit_value)]
 | 
			
		||||
 | 
			
		||||
pub mod components;
 | 
			
		||||
pub mod infra;
 | 
			
		||||
@ -9,7 +10,7 @@ use wasm_bindgen::prelude::{wasm_bindgen, JsValue};
 | 
			
		||||
 | 
			
		||||
#[wasm_bindgen]
 | 
			
		||||
pub fn run_app() -> Result<(), JsValue> {
 | 
			
		||||
    yew::start_app::<components::app::App>();
 | 
			
		||||
    yew::start_app::<components::app::AppContainer>();
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user