From f7ca1ecbc59b2264bacd593923166c680e881ffa Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 26 Aug 2025 09:57:58 -0400 Subject: [PATCH] chore: pc change --- Cargo.lock | 454 ++++++++++++++++++++++++- Cargo.toml | 4 + README.md | 102 +----- macros/Cargo.toml | 11 + macros/src/lib.rs | 78 +++++ public/loading.gif | Bin 0 -> 55535 bytes rust-analyzer.toml | 2 + rustfmt.toml | 1 + src/app.rs | 51 ++- src/common/form_definition.rs | 15 +- src/common/loaner_user.rs | 5 +- src/common/mod.rs | 10 +- src/common/student_info.rs | 6 +- src/common/user.rs | 25 ++ src/components/checkboxes.rs | 19 ++ src/components/loading.rs | 10 + src/components/mod.rs | 11 +- src/components/multi_entry.rs | 29 +- src/components/select.rs | 38 +++ src/components/text_field.rs | 25 +- src/lib.rs | 4 +- src/pages/home_page.rs | 193 +++++++---- src/pages/loaner_kiosk/loaner_kiosk.rs | 13 +- src/pages/loaner_kiosk/mod.rs | 5 +- src/pages/loaner_kiosk/user_entry.rs | 18 +- src/pages/mod.rs | 2 +- src/pages/provider_entry.rs | 57 ++-- 27 files changed, 926 insertions(+), 262 deletions(-) create mode 100644 macros/Cargo.toml create mode 100644 macros/src/lib.rs create mode 100644 public/loading.gif create mode 100644 rust-analyzer.toml create mode 100644 rustfmt.toml create mode 100644 src/common/user.rs create mode 100644 src/components/checkboxes.rs create mode 100644 src/components/loading.rs create mode 100644 src/components/select.rs diff --git a/Cargo.lock b/Cargo.lock index b1e26b9..927122b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "any_spawner" version = "0.3.0" @@ -565,7 +580,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -688,6 +703,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -858,6 +888,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -878,6 +943,18 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "default-struct-builder" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0df63c21a4383f94bd5388564829423f35c316aed85dc4f8427aded372c7c0d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.4.0" @@ -1134,8 +1211,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1482,6 +1561,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1600,6 +1703,12 @@ dependencies = [ "syn", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -1657,6 +1766,12 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "itertools" version = "0.10.5" @@ -1701,6 +1816,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "ring", + "serde", + "serde_json", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1734,7 +1862,7 @@ dependencies = [ "oco_ref", "or_poisoned", "paste", - "rand", + "rand 0.9.2", "reactive_graph", "rustc-hash 2.1.1", "rustc_version", @@ -1755,6 +1883,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "leptos-use" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6eac17e7d306b4ad67158aba97c1490884ba304add4321069cb63fe0834c3b1" +dependencies = [ + "cfg-if", + "codee", + "default-struct-builder", + "js-sys", + "lazy_static", + "leptos", + "paste", + "send_wrapper", + "thiserror 2.0.12", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "leptos_axum" version = "0.8.5" @@ -1878,6 +2026,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "leptos_oidc" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7797ff727c5b0b558faabe2bd784159b411803f56e384578d68471c699b85649" +dependencies = [ + "chrono", + "codee", + "jsonwebtoken", + "leptos", + "leptos-use", + "leptos_router", + "oauth2", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.12", + "tracing", +] + [[package]] name = "leptos_router" version = "0.8.5" @@ -1948,7 +2116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" dependencies = [ "cfg-if", - "windows-targets", + "windows-targets 0.53.3", ] [[package]] @@ -1985,6 +2153,14 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "macros" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "manyhow" version = "0.11.4" @@ -2129,6 +2305,25 @@ dependencies = [ "libc", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.16", + "http 1.3.1", + "rand 0.8.5", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + [[package]] name = "object" version = "0.36.7" @@ -2198,7 +2393,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2368,14 +2563,35 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -2385,7 +2601,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -2493,6 +2718,42 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + [[package]] name = "ring" version = "0.17.14" @@ -2693,9 +2954,12 @@ dependencies = [ "leptos", "leptos_axum", "leptos_meta", + "leptos_oidc", "leptos_router", + "macros", "serde", "tokio", + "url", "uuid", "wasm-bindgen", ] @@ -2999,6 +3263,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -3033,6 +3303,9 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3377,7 +3650,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", - "rand", + "rand 0.9.2", "sha1", "thiserror 2.0.12", "utf-8", @@ -3448,6 +3721,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -3678,13 +3952,92 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result", + "windows-strings 0.3.1", + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3693,7 +4046,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3702,14 +4055,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", ] [[package]] @@ -3718,48 +4088,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.7.12" diff --git a/Cargo.toml b/Cargo.toml index cfa8088..e02a014 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,10 @@ aws-sdk-dynamodb = { version = "1.74.0", optional = true } aws-config = { version = "1.6.2", optional = true } serde = "1.0.219" uuid = { version = "1.17.0", features = ["serde", "v4", "js"] } +leptos_oidc = "0.9.0" +url = "2.5.4" + +macros = { path = "./macros" } [features] hydrate = [ diff --git a/README.md b/README.md index a1d8ffd..0f8fb6e 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,21 @@ - - - Leptos Logo - +# Region 15 Scholarship Application (Rust Rewrite) -# Leptos Axum Starter Template +## Requirements +This repo requires that you have `cargo`, `clang`, and `node` installed. -This is a template for use with the [Leptos](https://github.com/leptos-rs/leptos) web framework and the [cargo-leptos](https://github.com/akesson/cargo-leptos) tool using [Axum](https://github.com/tokio-rs/axum). +- `cargo`: the project is written in Rust. +- `clang`: `ring`, a dependency of the `leptos_oidc` crate, requires this for compilation. +- `node`: required for `tailwindcss` compilation. -## Creating your template repo +## How to run +Run the command `cargo leptos watch` to compile and run an instance of the server on your local machine. To access info +from the AWS resources, you will need to be approved by me or someone else with AWS administrator access to obtain a +usable auth key from AWS IAM. -If you don't have `cargo-leptos` installed you can install it with +## Current Tasks +There are several tasks left to complete. These are a few of the larger ones: -```bash -cargo install cargo-leptos --locked -``` - -Then run -```bash -cargo leptos new --git https://github.com/leptos-rs/start-axum -``` - -to generate a new project template. - -```bash -cd scholarships-rs -``` - -to go to your newly created project. -Feel free to explore the project structure, but the best place to start with your application code is in `src/app.rs`. -Addtionally, Cargo.toml may need updating as new versions of the dependencies are released, especially if things are not working after a `cargo update`. - -## Running your project - -```bash -cargo leptos watch -``` - -## Installing Additional Tools - -By default, `cargo-leptos` uses `nightly` Rust, `cargo-generate`, and `sass`. If you run into any trouble, you may need to install one or more of these tools. - -1. `rustup toolchain install nightly --allow-downgrade` - make sure you have Rust nightly -2. `rustup target add wasm32-unknown-unknown` - add the ability to compile Rust to WebAssembly -3. `cargo install cargo-generate` - install `cargo-generate` binary (should be installed automatically in future) -4. `npm install -g sass` - install `dart-sass` (should be optional in future -5. Run `npm install` in end2end subdirectory before test - -## Compiling for Release -```bash -cargo leptos build --release -``` - -Will generate your server binary in target/server/release and your site package in target/site - -## Testing Your Project -```bash -cargo leptos end-to-end -``` - -```bash -cargo leptos end-to-end --release -``` - -Cargo-leptos uses Playwright as the end-to-end test tool. -Tests are located in end2end/tests directory. - -## Executing a Server on a Remote Machine Without the Toolchain -After running a `cargo leptos build --release` the minimum files needed are: - -1. The server binary located in `target/server/release` -2. The `site` directory and all files within located in `target/site` - -Copy these files to your remote server. The directory structure should be: -```text -scholarships-rs -site/ -``` -Set the following environment variables (updating for your project as needed): -```sh -export LEPTOS_OUTPUT_NAME="scholarships-rs" -export LEPTOS_SITE_ROOT="site" -export LEPTOS_SITE_PKG_DIR="pkg" -export LEPTOS_SITE_ADDR="127.0.0.1:3000" -export LEPTOS_RELOAD_PORT="3001" -``` -Finally, run the server binary. - -## Licensing - -This template itself is released under the Unlicense. You should replace the LICENSE for your own application with an appropriate license if you plan to release it publicly. +- Build the application's custom elements +- Build out the database requests +- Create proper authentication +- PDF generation using data (server-side) \ No newline at end of file diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..3fa6245 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = "2.0" +quote = "1.0" \ No newline at end of file diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..ed499b6 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,78 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{Data, Ident, parse_macro_input}; + +#[proc_macro_derive(Reactive)] +pub fn make_reactive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as syn::DeriveInput); + + let name = input.ident; + let reactive_name = Ident::new(&format!("{}Reactive", name), name.span()); + + let fields = if let Data::Struct(data) = input.data { + data.fields + } else { + panic!("Only structs can be reactive") + }; + + // Build the new fields. + let mut new_fields = Vec::new(); + let mut new_fn_args = Vec::new(); + let mut from_fn_args = Vec::new(); + + for field in fields { + let field_name = field.ident.unwrap(); + let field_type = field.ty; + + let new_field = quote! { + pub #field_name: _RwSignal<#field_type>, + }; + + new_fields.push(new_field); + + // Create new function arguments/inits + let new_fn_arg = quote! { + #field_name: _RwSignal::new(data.#field_name), + }; + + let from_fn_arg = quote! { + #field_name: self.#field_name.get_untracked(), + }; + + new_fn_args.push(new_fn_arg); + from_fn_args.push(from_fn_arg); + } + + let reactive_docs = format!("Reactive version of the [`{name}`] struct. Automatically created by the `Reactive` macro."); + + // Expand the macro. The output is a struct with new reactive fields and a new function, + // which initializes the reactive fields based on the original struct's fields. + let expanded = quote! { + use leptos::prelude::RwSignal as _RwSignal; // in case the user imports it themselves + use leptos::prelude::GetUntracked as _; + + #[doc = #reactive_docs] + pub struct #reactive_name { + #(#new_fields)* + } + + impl #reactive_name { + /// Creates a new `#reactive_name` struct from a non-reactive `#name`. + pub fn new(data: #name) -> Self { + Self { + #(#new_fn_args)* + } + } + + /// Captures the information of this struct into a `#name` struct, for use + /// in server functions. + pub fn capture(&self) -> #name { + #name { + #(#from_fn_args)* + } + } + } + }; + + TokenStream::from(expanded) +} diff --git a/public/loading.gif b/public/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..14c3be9e559cf92a660cb5f5fbf48309d2f2c9bd GIT binary patch literal 55535 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 literal 0 HcmV?d00001 diff --git a/rust-analyzer.toml b/rust-analyzer.toml new file mode 100644 index 0000000..ad17b4b --- /dev/null +++ b/rust-analyzer.toml @@ -0,0 +1,2 @@ +[rustfmt] +overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"] \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b9d4106 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = "2021" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index a9e3785..b95eacb 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,15 +1,15 @@ use leptos::prelude::*; use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; +use leptos_oidc::{ + Auth, AuthLoaded, AuthParameters, AuthSignal, Authenticated, Challenge, LoginLink, LogoutLink, +}; use leptos_router::{ components::{Route, Router, Routes}, StaticSegment, }; +use url::Url; -use crate::pages::{ - AboutPage, - HomePage, - ProviderEntry -}; +use crate::pages::{AboutPage, HomePage, ProviderEntry}; pub fn shell(options: LeptosOptions) -> impl IntoView { view! { @@ -34,17 +34,38 @@ pub fn App() -> impl IntoView { // Provides context that manages stylesheets, titles, meta tags, etc. provide_meta_context(); + // Authentication setup + let parameters = AuthParameters { + issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_Lfjuy5zaM".into(), + client_id: "10jr2h3vtpu9n7gj46pvg5qo2q".into(), + redirect_uri: Url::parse("http://localhost:3000").unwrap().to_string(), + post_logout_redirect_uri: Url::parse("http://localhost:3000").unwrap().to_string(), + scope: Some("openid%20profile%20email".into()), + audience: None, + challenge: Challenge::None, + }; + + let auth = Auth::signal(); + provide_context(auth); // allows use of this signal in lower areas of the tree without + // explicitly passing it through the html tree + + let _ = Auth::init(parameters); + view! { // injects a stylesheet into the document // id=leptos means cargo-leptos will hot-reload this stylesheet + //

"Authentication is loading"

+ // + // sets the document title // content for this welcome page <Router> <main> + // TODO Create a 404 page <Routes fallback=|| "Page not found.".into_view()> <Route path=StaticSegment("") view=HomePage/> <Route path=StaticSegment("about") view=AboutPage/> @@ -54,3 +75,23 @@ pub fn App() -> impl IntoView { </Router> } } + +#[component] +pub fn Unauthenticated() -> impl IntoView { + view! { + <p>"Error 401: Unauthorized"</p> + <LoginLink class="text-login">"Sign in"</LoginLink> + } +} + +#[component] +pub fn AuthErrorView() -> impl IntoView { + let auth = use_context::<AuthSignal>().expect("AuthError: RwSignal<AuthStore> was not found"); + + let error_message = move || auth.get().error().map(|error| format!("{error:?}")); + + view! { + <h1>"An authentication error occurred."</h1> + {error_message} + } +} diff --git a/src/common/form_definition.rs b/src/common/form_definition.rs index 3e72a20..33fab4c 100644 --- a/src/common/form_definition.rs +++ b/src/common/form_definition.rs @@ -1,15 +1,14 @@ +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use serde::{Serialize, Deserialize}; - #[derive(Serialize, Deserialize)] struct FormDefinition { - sections: Vec<Section> + sections: Vec<Section>, } #[derive(Serialize, Deserialize)] struct Section { - questions: Vec<QuestionType> + questions: Vec<QuestionType>, } #[derive(Serialize, Deserialize)] @@ -20,12 +19,10 @@ enum QuestionType { Dropdown, Checkbox, Radio, - Multi + Multi, } -struct TextInput { - -} +struct TextInput {} #[derive(Serialize, Deserialize)] struct QuestionComponent { @@ -34,5 +31,5 @@ struct QuestionComponent { placeholder: Option<String>, // Used in <select> and <checkbox> elements - options: Option<HashMap<String, String>> + options: Option<HashMap<String, String>>, } diff --git a/src/common/loaner_user.rs b/src/common/loaner_user.rs index ce5cd0b..a917743 100644 --- a/src/common/loaner_user.rs +++ b/src/common/loaner_user.rs @@ -1,10 +1,9 @@ - #[derive(Clone)] pub struct LoanerUser { pub first_name: String, pub last_name: String, pub email: String, - pub has_loan: bool + pub has_loan: bool, } impl LoanerUser { @@ -13,7 +12,7 @@ impl LoanerUser { first_name: "".to_string(), last_name: "".to_string(), email: "".to_string(), - has_loan: false + has_loan: false, } } } diff --git a/src/common/mod.rs b/src/common/mod.rs index aec0d04..6deae78 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,8 +1,6 @@ -mod loaner_user; -mod student_info; mod form_definition; +mod loaner_user; +mod student_info; +mod user; -pub use self::{ - loaner_user::*, - student_info::* -}; +pub use self::{loaner_user::*, student_info::*, user::*}; diff --git a/src/common/student_info.rs b/src/common/student_info.rs index a61e51a..eb25697 100644 --- a/src/common/student_info.rs +++ b/src/common/student_info.rs @@ -1,7 +1,9 @@ -use serde::{Deserialize, Serialize}; +use macros::Reactive; +use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Reactive)] pub struct StudentInfo { pub first_name: String, pub last_name: String, + pub math_sat_score: i32, } diff --git a/src/common/user.rs b/src/common/user.rs new file mode 100644 index 0000000..49a99bf --- /dev/null +++ b/src/common/user.rs @@ -0,0 +1,25 @@ +use serde::{Deserialize, Serialize}; + +// API URL to get user information from Google's Groups API: +// https://admin.googleapis.com/admin/directory/v1/groups?domain=region15.org&userKey=<email> +// Note that this requires a service account of some type, and that it should only be called from +// a pre-sign up lambda function. + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserClaims { + /// The time the JWT was issued, in UTC time. + #[serde(rename = "iat")] + issued_at: usize, + /// The issuer of the JWT. + #[serde(rename = "iss")] + issuer: String, + /// The time the JWT expires, in UTC time. + #[serde(rename = "exp")] + expiration_at: usize, + /// The subject of the JWT. This is the only unique identifier for a user. + #[serde(rename = "sub")] + pub subject: String, + /// The username of the user. This should be their email address. + #[serde(rename = "username")] + pub username: String, +} diff --git a/src/components/checkboxes.rs b/src/components/checkboxes.rs new file mode 100644 index 0000000..22d820d --- /dev/null +++ b/src/components/checkboxes.rs @@ -0,0 +1,19 @@ +use leptos::prelude::*; + +#[component] +pub fn Checkbox( + #[prop()] checked: ReadSignal<bool>, + #[prop()] on_change: Callback<bool>, + #[prop()] label: String, + #[prop()] id: String +) -> impl IntoView { + let label_id = id.clone(); + view! { + <label for=label_id> + <input + type="checkbox" + id={id} + /> + </label> + } +} diff --git a/src/components/loading.rs b/src/components/loading.rs new file mode 100644 index 0000000..caa82be --- /dev/null +++ b/src/components/loading.rs @@ -0,0 +1,10 @@ +use leptos::prelude::*; + +#[component] +pub fn Loading() -> impl IntoView { + view! { + <div> + <img src="loading.gif" /> + </div> + } +} diff --git a/src/components/mod.rs b/src/components/mod.rs index 4e8650b..d4d4e97 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -1,9 +1,8 @@ mod action_button; -mod text_field; +mod loading; mod multi_entry; +mod text_field; +mod select; +mod checkboxes; -pub use self::{ - action_button::*, - text_field::*, - multi_entry::* -}; +pub use self::{action_button::*, loading::*, multi_entry::*, text_field::*, select::*}; diff --git a/src/components/multi_entry.rs b/src/components/multi_entry.rs index a842699..0a9a5ce 100644 --- a/src/components/multi_entry.rs +++ b/src/components/multi_entry.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; +use crate::components::ActionButton; use leptos::{leptos_dom::logging::console_log, prelude::*}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::components::ActionButton; #[derive(Clone, Serialize, Deserialize, Debug)] pub struct MultiEntryData { @@ -27,7 +27,7 @@ pub struct MultiEntryMember { /// The name of the member, used for display only. pub display_name: String, /// The actual name of the member, used to get the data from an Entry struct. - pub member_name: String + pub member_name: String, } impl MultiEntryMember { @@ -35,7 +35,7 @@ impl MultiEntryMember { pub fn new() -> Self { Self { display_name: String::new(), - member_name: String::new() + member_name: String::new(), } } @@ -43,7 +43,7 @@ impl MultiEntryMember { pub fn from(display_name: String, member_name: String) -> Self { Self { display_name, - member_name + member_name, } } @@ -51,7 +51,7 @@ impl MultiEntryMember { pub fn from_str(display_name: &str, member_name: &str) -> Self { Self { display_name: display_name.into(), - member_name: member_name.into() + member_name: member_name.into(), } } } @@ -85,9 +85,6 @@ pub fn Entry( /// The secondary display member for this entry. #[prop()] info_member: Signal<MultiEntryMember>, - /// The child components of this Entry view. - #[prop()] - children: Children ) -> impl IntoView { let nameData = entry_data.data.get(&name_member.get().member_name).unwrap(); let infoData = entry_data.data.get(&info_member.get().member_name).unwrap(); @@ -111,17 +108,19 @@ pub fn MultiEntry( name_member: Signal<MultiEntryMember>, /// The secondary display member for each entry object. #[prop()] - info_member: Signal<MultiEntryMember> + info_member: Signal<MultiEntryMember>, ) -> impl IntoView { // Creates an entry in the given entries list. let create_entry = move |_| { let mut new_entry = MultiEntryData::new(); - new_entry - .data - .insert(name_member.get().member_name, ValueType::String(String::from("Testing"))); - new_entry - .data - .insert(info_member.get().member_name, ValueType::String(new_entry.id.to_string())); + new_entry.data.insert( + name_member.get().member_name, + ValueType::String(String::from("Testing")), + ); + new_entry.data.insert( + info_member.get().member_name, + ValueType::String(new_entry.id.to_string()), + ); entries.update(|entry_vec| entry_vec.push(new_entry)); }; diff --git a/src/components/select.rs b/src/components/select.rs new file mode 100644 index 0000000..4749266 --- /dev/null +++ b/src/components/select.rs @@ -0,0 +1,38 @@ +use leptos::prelude::*; + +/// A custom-styled select input. +/// +/// Takes a `Vec<String>` as the set of inputs. Only one value may be selected, which is indicated +/// by the `value` prop of the component. +#[component] +pub fn Select( + #[prop(default = vec!["Test Value".into()])] value_list: Vec<String>, + #[prop(default = RwSignal::new("".into()))] value: RwSignal<String>, + #[prop(default = RwSignal::new("".into()))] label: RwSignal<String>, + #[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>, +) -> impl IntoView { + view! { + <div> + <label> + {label} + <select + class="relative border-2 m-1.5 p-1.5 rounded-md" + class=(["border-red-700", "bg-transparent"], move || !disabled.get()) + class=(["border-gray-600", "pointer-events-none", "bg-gray-600/33"], move || disabled.get()) + on:change:target=move |e| { + value.set(e.target().value()); + }> + // This closure handles the display of the options. + {move || { + value_list.iter().map( + move |value| { + let display = value.to_owned(); + view! { <option value={display}>{display.clone()}</option> } + } + ).collect::<Vec<_>>() + }} + </select> + </label> + </div> + } +} diff --git a/src/components/text_field.rs b/src/components/text_field.rs index 12ddf59..ca4eca6 100644 --- a/src/components/text_field.rs +++ b/src/components/text_field.rs @@ -1,26 +1,39 @@ +use std::str::FromStr; use leptos::prelude::*; #[component] -pub fn OutlinedTextField( +pub fn OutlinedTextField<T>( #[prop(default = String::from(""))] placeholder: String, - #[prop(default = RwSignal::new("".to_string()))] value: RwSignal<String>, + #[prop(default = RwSignal::new(T::default()))] value: RwSignal<T>, #[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>, #[prop(default = String::from(""))] name: String, #[prop(default = String::from(""))] label: String, #[prop(default = String::from(""))] input_type: String, -) -> impl IntoView { +) -> impl IntoView +where + T: Default + 'static + FromStr + ToString + PartialEq + Send + Sync + Clone +{ + // Parsing logic for non-string types which happens on each input. + let on_input = move |e| { + let input_value = event_target_value(&e); + let parsed_value = T::from_str(&input_value) + .unwrap_or_default(); + value.set(parsed_value); + }; + view! ( <label> { label } <input class="border-2 m-1.5 p-1.5 rounded-md bg-transparent mt-6 relative transition-all duration-400" - class=(["border-red-700"], move || !disabled.get()) - class=(["border-gray-600", "pointer-events-none"], move || disabled.get()) + class=(["border-red-700", "bg-transparent"], move || !disabled.get()) + class=(["border-gray-600", "pointer-events-none", "bg-gray-600/33"], move || disabled.get()) r#type={input_type} disabled={disabled} placeholder={placeholder} prop:name=name - bind:value=value // This is a two-way binding - it is allowed to read *and* write. + prop:value={move || value.get().to_string()} // Sets the initial value. + on:input=on_input // Sets the value on each input. /> </label> ) diff --git a/src/lib.rs b/src/lib.rs index 7b40348..e1cbf90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ +use leptos::prelude::event_target_value; + pub mod app; +mod common; mod components; mod pages; -mod common; #[cfg(feature = "hydrate")] #[wasm_bindgen::prelude::wasm_bindgen] diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 16eb006..23c867d 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -1,38 +1,54 @@ -use crate::components::{ActionButton, OutlinedTextField}; -use crate::common::StudentInfo; -use leptos::leptos_dom::logging::console_log; -use leptos::prelude::*; - +use std::collections::HashMap; #[cfg(feature = "ssr")] use aws_sdk_dynamodb::{error::ProvideErrorMetadata, types::AttributeValue, Client}; +use crate::app::Unauthenticated; +use crate::common::{StudentInfo, StudentInfoReactive, UserClaims}; +use crate::components::{ActionButton, Loading, OutlinedTextField, Select}; +use leptos::leptos_dom::logging::console_log; +use leptos::prelude::*; +use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated}; #[server(GetSubmission, endpoint = "/get-submission")] pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> { let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; let dbclient = Client::new(&config); - console_log(format!("Getting values from API using key {}", id).as_str()); + console_log(format!("Getting values from API using username {}", id).as_str()); match dbclient .get_item() .table_name("student-applications") - .key("Email", AttributeValue::S(id)) + .key("Email", AttributeValue::S(id.clone())) .send() .await { Ok(output) => { - console_log("Found submission with passed key."); let item = output.item.unwrap(); - let first_name = item + + let first_name: String = item .get("studentFirstName") - .unwrap() - .as_s() - .unwrap() - .clone(); - let last_name = item.get("studentLastName").unwrap().as_s().unwrap().clone(); + .and_then(|attr| attr.as_s().ok()) + .map(|s| s.to_owned()) + .unwrap_or_default(); + + let last_name: String = item + .get("studentLastName") + .and_then(|attr| attr.as_s().ok()) + .map(|s| s.to_owned()) + .unwrap_or_default(); + + let math_sat_score = item + .get("mathSAT") + .and_then(|attr| attr.as_n().ok()) + .map(|n| n.parse::<i32>().unwrap_or(0)) + .unwrap_or_default(); + + console_log(format!("Got values from API: {} {}", first_name, last_name).as_str()); + Ok(StudentInfo { first_name, last_name, + math_sat_score, }) } Err(err) => { @@ -43,7 +59,10 @@ pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> { } #[server(CreateSampleSubmission, endpoint = "/create-sample-submission")] -pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), ServerFnError> { +pub async fn create_sample_submission( + student_info: StudentInfo, + subject: String, +) -> Result<(), ServerFnError> { use aws_sdk_dynamodb::{types::AttributeValue, Client}; let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; @@ -51,18 +70,16 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S console_log( format!( - "Creating sample submission with name {} {}", - student_info.first_name, student_info.last_name + "Creating sample submission with name {} {}, math score {}, and subject {}", + student_info.first_name, student_info.last_name, student_info.math_sat_score, subject ) .as_str(), ); - let submission_id = String::from("testing_student"); - match dbclient .update_item() .table_name("student-applications") - .key("Email", AttributeValue::S(submission_id)) + .key("Email", AttributeValue::S(subject)) .expression_attribute_values( ":studentFirstName", AttributeValue::S(student_info.first_name), @@ -71,10 +88,17 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S ":studentLastName", AttributeValue::S(student_info.last_name), ) + .expression_attribute_values( + ":mathScoreSAT", + AttributeValue::N(student_info.math_sat_score.to_string()), + ) .expression_attribute_names("#studentFirstName", "studentFirstName") .expression_attribute_names("#studentLastName", "studentLastName") + .expression_attribute_names("#mathScoreSAT", "mathScoreSAT") .update_expression( - "SET #studentFirstName = :studentFirstName, #studentLastName = :studentLastName", + concat!("SET #studentFirstName = :studentFirstName, ", + "#studentLastName = :studentLastName, ", + "#mathScoreSAT = :mathScoreSAT"), ) .send() .await @@ -86,65 +110,98 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S #[component] pub fn HomePage() -> impl IntoView { - let sample_id = RwSignal::new("google_113247439743075864879".to_string()); // Creates a reactive value to update the button - let count = RwSignal::new(0); - let elements_disabled = RwSignal::new(false); + let auth = use_context::<AuthSignal>().expect("Couldn't find user information."); + let user_claims = Signal::derive(move || { + auth.with(|auth| { + auth.authenticated().and_then(|data| { + data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"]) + }) + }) + }); + + // Note that the value passed in MUST be equatable. + // We get/unwrap the value repeatedly until we get a simple string value, then clone it so that + // we don't lose access to it in the future, should we need it again. let server_resource = Resource::new( - move || sample_id.get(), - async |id| { - get_submission(id).await.unwrap_or_else(|e| { + move || user_claims.get().map(|claim| claim.claims.subject.clone()), + async |opt_username| match opt_username { + Some(username) => get_submission(username).await.unwrap_or_else(|e| { console_log(e.to_string().as_str()); StudentInfo { first_name: String::from("Error"), last_name: String::from(""), + math_sat_score: 0, } - }) + }), + None => StudentInfo { + first_name: String::new(), + last_name: String::new(), + math_sat_score: 0, + }, }, ); - let on_click = move |_| { - *count.write() += 1; - *elements_disabled.write() = count.get() == 10; - }; let submit_action = ServerAction::<CreateSampleSubmission>::new(); view! { - <h1>"Welcome to Leptos!"</h1> - <p> - "Lillian's reported full name from the API: " - <Suspense fallback=move || view! { <span>"Loading..."</span> }> - {move || { - server_resource.get().map(|submission| { - view! { <span>{submission.first_name}" "{submission.last_name}</span> } - }) - }} - </Suspense> - </p> - <ActionForm action=submit_action> - <div> - <OutlinedTextField - label="First Name".into() - placeholder="John".into() - disabled={elements_disabled} - name="student_info[first_name]".into() /> - </div> - <div> - <OutlinedTextField - label="Last Name".into() - placeholder="Smith".into() - disabled={elements_disabled} - name="student_info[last_name]".into() /> - </div> - <div> - <ActionButton - on:click=on_click - disabled={elements_disabled} - button_type="submit".into() - > - "Submit" - </ActionButton> - </div> - </ActionForm> + <AuthLoaded fallback=Loading> + <Authenticated unauthenticated=Unauthenticated> + // Replace this fallback with a real loading screen. + <Suspense fallback=Loading> + {move || { + server_resource.get().map(|submission: StudentInfo| { + let reactive_info = StudentInfoReactive::new(submission); + + let select_value = RwSignal::new(String::from("Math")); + + view! { + <p> + "Current user's reported full name from the API: " + {reactive_info.first_name}" "{reactive_info.last_name} + </p> + + <form> + <div> + <OutlinedTextField + label="First Name".into() + placeholder="John".into() + value=reactive_info.first_name /> + </div> + <div> + <OutlinedTextField + label="Last Name".into() + placeholder="Smith".into() + value=reactive_info.last_name /> + </div> + <div> + <OutlinedTextField + label="Testing".into() + placeholder="Test".into() + value=reactive_info.math_sat_score /> + </div> + <Select + value_list = vec!["Math".into(), "English".into(), "Science".into()] + value = select_value + /> + <div> + <ActionButton + on:click=move |_| { + submit_action.dispatch(CreateSampleSubmission { + student_info: reactive_info.capture(), + subject: user_claims.get().unwrap().claims.subject.clone() + }); + } + > + "Submit" + </ActionButton> + </div> + </form> + } + }) + }} + </Suspense> + </Authenticated> + </AuthLoaded> } } diff --git a/src/pages/loaner_kiosk/loaner_kiosk.rs b/src/pages/loaner_kiosk/loaner_kiosk.rs index 6bdfdac..c2f2f4e 100644 --- a/src/pages/loaner_kiosk/loaner_kiosk.rs +++ b/src/pages/loaner_kiosk/loaner_kiosk.rs @@ -1,23 +1,20 @@ -//! This defines the base component of the Chromebook loaner kiosk application. +//! This defines the base component of the Chromebook loaner kiosk application. //! There are going to be 3 definitive screens that the users are able to see and use: //! - The username entry screen: users will enter their name and email. //! - The action selection screen: users will be given a choice regarding their loaner status. //! - The completion screen: users will be given a success message should their information save. //! Refer to each component's file for more information about how they work. -use leptos::prelude::*; +use super::{action_selection::ActionSelection, user_entry::UserEntry}; use crate::common::LoanerUser; -use super::{ - user_entry::UserEntry, - action_selection::ActionSelection -}; +use leptos::prelude::*; /// Used to keep track of the currently displayed controls. #[derive(Clone, Copy, PartialEq)] pub enum LoanerKioskState { UserEntry, ActionSelection, - Complete + Complete, } /// The base Chromebook loaner kiosk component. @@ -27,7 +24,7 @@ pub enum LoanerKioskState { pub fn LoanerKiosk() -> impl IntoView { let current_state = RwSignal::new(LoanerKioskState::UserEntry); let user = RwSignal::new(LoanerUser::new()); - + view! { <div class="flex w-screen h-screen justify-center items-center"> <UserEntry diff --git a/src/pages/loaner_kiosk/mod.rs b/src/pages/loaner_kiosk/mod.rs index d4f6ff5..2dfbb58 100644 --- a/src/pages/loaner_kiosk/mod.rs +++ b/src/pages/loaner_kiosk/mod.rs @@ -1,4 +1,3 @@ -mod loaner_kiosk; -mod user_entry; mod action_selection; - +mod loaner_kiosk; +mod user_entry; diff --git a/src/pages/loaner_kiosk/user_entry.rs b/src/pages/loaner_kiosk/user_entry.rs index ba979be..17e62ae 100644 --- a/src/pages/loaner_kiosk/user_entry.rs +++ b/src/pages/loaner_kiosk/user_entry.rs @@ -1,4 +1,4 @@ -//! Contains the user entry component. +//! Contains the user entry component. //! It would be preferred if the user was restricted to a real user account. This is the URL: //! https://admin.googleapis.com/admin/directory/v1/users?domain=region15.org&projection=BASIC&query=isArchived=false%20isAdmin=false //! Once they select this, they can then check in/out the loaners under their account. @@ -6,10 +6,10 @@ //! a service account. That's going to be a pain to set up. //! For example's sake here, we're just going to take a first name, last name, and email address. -use leptos::leptos_dom::logging::console_log; -use leptos::prelude::*; use crate::common::LoanerUser; use crate::components::{ActionButton, OutlinedTextField}; +use leptos::leptos_dom::logging::console_log; +use leptos::prelude::*; /// The user entry component for the loaner kiosk. /// @@ -20,11 +20,14 @@ use crate::components::{ActionButton, OutlinedTextField}; #[component] pub fn UserEntry( /// A callback function used when the control has determined this stage is finished. - #[prop()] mut on_submit: impl FnMut() + 'static, + #[prop()] + mut on_submit: impl FnMut() + 'static, /// The global user record. - #[prop()] user: RwSignal<LoanerUser>, + #[prop()] + user: RwSignal<LoanerUser>, /// Whether this control is shown. Required for use with CSS animations. - #[prop()] shown: bool + #[prop()] + shown: bool, ) -> impl IntoView { let disabled = RwSignal::new(false); let on_click = move |_| { @@ -44,16 +47,19 @@ pub fn UserEntry( label="First Name".into() placeholder="John".into() disabled={disabled} + value=RwSignal::new("".to_owned()) /> <OutlinedTextField label="Last Name".into() placeholder="Smith".into() disabled={disabled} + value=RwSignal::new("".to_owned()) /> <OutlinedTextField label="Region 15 Email".into() placeholder="jsmith@region15.org".into() disabled={disabled} + value=RwSignal::new("".to_owned()) /> <ActionButton on:click=on_click diff --git a/src/pages/mod.rs b/src/pages/mod.rs index a467f97..1650220 100644 --- a/src/pages/mod.rs +++ b/src/pages/mod.rs @@ -1,6 +1,6 @@ mod about_page; mod home_page; -mod provider_entry; mod loaner_kiosk; +mod provider_entry; pub use self::{about_page::*, home_page::*, provider_entry::*}; diff --git a/src/pages/provider_entry.rs b/src/pages/provider_entry.rs index 29620cf..b93c93b 100644 --- a/src/pages/provider_entry.rs +++ b/src/pages/provider_entry.rs @@ -1,27 +1,46 @@ -use crate::components::{MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField}; +use crate::app::Unauthenticated; +use crate::common::UserClaims; +use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField}; +use leptos::leptos_dom::logging::console_log; use leptos::prelude::*; +use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated, LogoutLink}; +use serde::{Deserialize, Serialize}; #[component] pub fn ProviderEntry() -> impl IntoView { + let auth = use_context::<AuthSignal>().expect("Couldn't find auth signal"); + let auth_data = auth + .get() + .authenticated() + .and_then(|data| data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"])); + let multi_entries: RwSignal<Vec<MultiEntryData>> = RwSignal::new(vec![]); - + + console_log(format!("User claims: {auth_data:?}").as_str()); + view! ( - <h1>This is the provider entry page</h1> - <h2>It would usually have a series of questions that we ask the providers</h2> - <div> - <OutlinedTextField - placeholder = "Testing information...".into() - name = "testing_input".into() - label = "This is a testing question.".into() - /> - </div> - <div> - <p>This is an example of the multientry element.</p> - <MultiEntry - entries=multi_entries - name_member=Signal::from(MultiEntryMember::from_str("Entry Name", "name")) - info_member=Signal::from(MultiEntryMember::from_str("Entry Info", "info")) - /> - </div> + <AuthLoaded fallback=Loading> + <Authenticated unauthenticated=Unauthenticated> + <LogoutLink class="text-logout">"Sign out"</LogoutLink> + <h1>This is the provider entry page</h1> + <h2>It would usually have a series of questions that we ask the providers</h2> + <div> + <OutlinedTextField + placeholder = "Testing information...".into() + name = "testing_input".into() + label = "This is a testing question.".into() + value = RwSignal::new("".to_owned()) + /> + </div> + <div> + <p>This is an example of the multientry element.</p> + <MultiEntry + entries=multi_entries + name_member=Signal::from(MultiEntryMember::from_str("Entry Name", "name")) + info_member=Signal::from(MultiEntryMember::from_str("Entry Info", "info")) + /> + </div> + </Authenticated> + </AuthLoaded> ) }