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 0000000..14c3be9 Binary files /dev/null and b/public/loading.gif differ 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> ) }