From f93eb3c4293996f7f0697a4849a128bb7315752c Mon Sep 17 00:00:00 2001 From: Avii Date: Thu, 17 Oct 2024 00:56:02 +0200 Subject: [PATCH] avam-client and oauth2 --- .vscode/launch.json | 117 + .vscode/settings.json | 2 +- Cargo.lock | 2755 ++++++++++++++++- Cargo.toml | 15 +- avam-client/Cargo.toml | 37 + avam-client/build.rs | 5 + avam-client/icon.ico | Bin 0 -> 15406 bytes avam-client/icon.rc | 1 + avam-client/src/app.rs | 144 + avam-client/src/config.rs | 125 + avam-client/src/dirs.rs | 43 + avam-client/src/icon.rs | 117 + avam-client/src/lock.rs | 48 + avam-client/src/main.rs | 180 ++ avam-client/src/models.rs | 300 ++ avam-client/src/oauth.rs | 95 + avam-client/src/pipe.rs | 97 + avam-client/src/state_machine.rs | 149 + migrations/20241012185353_oauth2.down.sql | 5 + migrations/20241012185353_oauth2.up.sql | 40 + src/bin/server/main.rs | 8 +- src/lib/config.rs | 5 + src/lib/domain/api/mod.rs | 2 + src/lib/domain/api/models/mod.rs | 2 + src/lib/domain/api/models/oauth.rs | 377 +++ src/lib/domain/api/models/pilot.rs | 0 src/lib/domain/api/models/user.rs | 25 +- src/lib/domain/api/ports.rs | 140 +- src/lib/domain/api/ports/api_service.rs | 60 + src/lib/domain/api/ports/oauth_repository.rs | 33 + src/lib/domain/api/ports/user_notifier.rs | 12 + src/lib/domain/api/ports/user_repository.rs | 62 + src/lib/domain/api/service.rs | 99 +- src/lib/domain/leptos/app.rs | 45 +- src/lib/domain/leptos/app/pages/auth/login.rs | 35 +- .../domain/leptos/app/pages/auth/logout.rs | 35 +- src/lib/domain/leptos/app/pages/auth/reset.rs | 4 +- src/lib/domain/leptos/app/pages/mod.rs | 1 + .../leptos/app/pages/oauth2/authorize.rs | 279 ++ src/lib/domain/leptos/app/pages/oauth2/mod.rs | 1 + src/lib/domain/leptos/flashbag.rs | 8 +- src/lib/inbound/http.rs | 5 +- src/lib/inbound/http/handlers/mod.rs | 1 + src/lib/inbound/http/handlers/oauth.rs | 192 ++ src/lib/lib.rs | 2 + .../dangerous_lettre/user_notifier.rs | 5 +- src/lib/outbound/postgres.rs | 1 + src/lib/outbound/postgres/oauth_repository.rs | 179 ++ src/lib/outbound/postgres/user_repository.rs | 8 +- templates/user/user_notifier.rs | 50 - 50 files changed, 5674 insertions(+), 277 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 avam-client/Cargo.toml create mode 100644 avam-client/build.rs create mode 100644 avam-client/icon.ico create mode 100644 avam-client/icon.rc create mode 100644 avam-client/src/app.rs create mode 100644 avam-client/src/config.rs create mode 100644 avam-client/src/dirs.rs create mode 100644 avam-client/src/icon.rs create mode 100644 avam-client/src/lock.rs create mode 100644 avam-client/src/main.rs create mode 100644 avam-client/src/models.rs create mode 100644 avam-client/src/oauth.rs create mode 100644 avam-client/src/pipe.rs create mode 100644 avam-client/src/state_machine.rs create mode 100644 migrations/20241012185353_oauth2.down.sql create mode 100644 migrations/20241012185353_oauth2.up.sql create mode 100644 src/lib/domain/api/models/oauth.rs create mode 100644 src/lib/domain/api/models/pilot.rs create mode 100644 src/lib/domain/api/ports/api_service.rs create mode 100644 src/lib/domain/api/ports/oauth_repository.rs create mode 100644 src/lib/domain/api/ports/user_notifier.rs create mode 100644 src/lib/domain/api/ports/user_repository.rs create mode 100644 src/lib/domain/leptos/app/pages/oauth2/authorize.rs create mode 100644 src/lib/domain/leptos/app/pages/oauth2/mod.rs create mode 100644 src/lib/inbound/http/handlers/oauth.rs create mode 100644 src/lib/outbound/postgres/oauth_repository.rs delete mode 100644 templates/user/user_notifier.rs diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..40c6da6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,117 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'avam'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=avam" + ], + "filter": { + "name": "avam", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'avam'", + "cargo": { + "args": [ + "build", + "--bin=avam", + "--package=avam" + ], + "filter": { + "name": "avam", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'avam'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=avam", + "--package=avam" + ], + "filter": { + "name": "avam", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'avam-client'", + "cargo": { + "args": [ + "build", + "--bin=avam-client", + "--package=avam-client" + ], + "filter": { + "name": "avam-client", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'avam-client'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=avam-client", + "--package=avam-client" + ], + "filter": { + "name": "avam-client", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'avam_wasm'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=avam-wasm" + ], + "filter": { + "name": "avam_wasm", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a271808..2462b61 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "leptos_macro": [ // optional: // "component", - "server" + "server", ], }, // if code that is cfg-gated for the `ssr` feature is shown as inactive, diff --git a/Cargo.lock b/Cargo.lock index d623f51..e924b48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ab_glyph" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + [[package]] name = "addr2line" version = "0.24.2" @@ -11,6 +27,12 @@ dependencies = [ "gimli", ] +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.0" @@ -59,6 +81,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -73,12 +96,45 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "allocator-api2" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.6.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -94,6 +150,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.89" @@ -106,6 +211,23 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "argon2" version = "0.5.3" @@ -118,6 +240,24 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + [[package]] name = "async-recursion" version = "1.1.1" @@ -140,6 +280,29 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "atk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4af014b17dd80e8af9fa689b2d4a211ddba6eb583c1622f35d0cb543f6b17e4" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251e0b7d90e33e0ba930891a505a9a35ece37b2dd37a14f3ffc306c13b980009" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "atoi" version = "2.0.0" @@ -149,6 +312,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "attribute-derive" version = "0.9.2" @@ -185,6 +354,20 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + [[package]] name = "avam" version = "0.1.0" @@ -195,9 +378,11 @@ dependencies = [ "axum-macros", "axum_session", "axum_session_sqlx", + "base64 0.22.1", "derive_more", "dotenvy", "http 1.1.0", + "jsonwebtoken", "leptos", "leptos_axum", "leptos_meta", @@ -205,9 +390,12 @@ dependencies = [ "lettre", "rand", "serde", + "serde_qs 0.13.0", + "sha256", "sqlx", "tera", "thiserror", + "time", "tokio", "tower 0.4.13", "tower-http", @@ -218,6 +406,35 @@ dependencies = [ "validator", ] +[[package]] +name = "avam-client" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "clap", + "config", + "ctrlc", + "derive_more", + "directories", + "image", + "interprocess", + "open", + "rand", + "reqwest", + "serde", + "serde_qs 0.13.0", + "sha256", + "thiserror", + "tokio", + "toml", + "tray-icon", + "uuid", + "windres", + "winit", + "winreg 0.52.0", +] + [[package]] name = "avam-wasm" version = "0.1.0" @@ -230,6 +447,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "avif-serialize" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +dependencies = [ + "arrayvec", +] + [[package]] name = "axum" version = "0.7.7" @@ -306,7 +532,7 @@ dependencies = [ "aes-gcm", "async-trait", "axum", - "base64", + "base64 0.22.1", "bytes", "chrono", "cookie", @@ -349,12 +575,18 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.0", "object", "rustc-demangle", "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -376,6 +608,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -385,6 +629,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitstream-io" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" + [[package]] name = "blake2" version = "0.10.6" @@ -403,6 +653,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "bstr" version = "1.10.0" @@ -413,18 +672,36 @@ dependencies = [ "serde", ] +[[package]] +name = "built" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "236e6289eda5a812bc6b53c3b024039382a2895fbbeef2d748b2931546d392c4" + [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytemuck" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.7.2" @@ -463,6 +740,57 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.6.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.6.0", + "log", + "polling", + "rustix", + "slab", + "thiserror", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix", + "wayland-backend", + "wayland-client", +] + [[package]] name = "camino" version = "1.1.9" @@ -475,15 +803,39 @@ version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -544,12 +896,80 @@ dependencies = [ "inout", ] +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + [[package]] name = "collection_literals" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concat-string" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -565,12 +985,18 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ + "async-trait", "convert_case", + "json5", "lazy_static", "nom", "pathdiff", + "ron", + "rust-ini", "serde", + "serde_json", "toml", + "yaml-rust", ] [[package]] @@ -589,6 +1015,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "const_format" version = "0.2.33" @@ -625,7 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "aes-gcm", - "base64", + "base64 0.22.1", "percent-encoding", "rand", "subtle", @@ -633,12 +1079,80 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.14" @@ -663,6 +1177,24 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -723,6 +1255,22 @@ dependencies = [ "cipher", ] +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix", + "windows-sys 0.59.0", +] + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + [[package]] name = "darling" version = "0.14.4" @@ -743,7 +1291,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn 1.0.109", ] @@ -850,6 +1398,42 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "displaydoc" version = "0.2.5" @@ -861,12 +1445,48 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.5", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25c0e292a7ca6d6498557ff1df68f32c99850012b6ea401cf8daf771f22ff53" + [[package]] name = "drain_filter_polyfill" version = "0.1.3" @@ -888,7 +1508,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" dependencies = [ - "base64", + "base64 0.22.1", "memchr", ] @@ -945,12 +1565,68 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide 0.7.4", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +[[package]] +name = "fdeflate" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-winsdk" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8cbf17b871570c1f8612b763bac3e86290602bcf5dc3c5ce657e0e1e9071d9e" +dependencies = [ + "serde", + "serde_derive", + "winreg 0.5.1", +] + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + [[package]] name = "flume" version = "0.11.0" @@ -968,6 +1644,33 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1087,6 +1790,64 @@ dependencies = [ "slab", ] +[[package]] +name = "gdk" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ba081bdef3b75ebcdbfc953699ed2d7417d6bd853347a42a37d76406a33646" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ff856cb3386dae1703a920f803abafcc580e9b5f711ca62ed1620c25b51ff2" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1097,6 +1858,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1120,12 +1891,101 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.6.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "globset" version = "0.4.15" @@ -1145,7 +2005,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags", + "bitflags 2.6.0", "ignore", "walkdir", ] @@ -1350,6 +2210,69 @@ dependencies = [ "web-sys", ] +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c4f5e0e20b60e10631a5f06da7fe3dda744b05ad0ea71fee2f47adf865890c" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771437bf1de2c1c0b496c11505bdf748e26066bbe942dfc8f614c9460f6d7722" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "half" version = "2.4.1" @@ -1391,6 +2314,12 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -1403,6 +2332,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -1536,6 +2471,25 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", ] [[package]] @@ -1545,13 +2499,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", + "futures-channel", "futures-util", "http 1.1.0", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -1739,6 +2696,45 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" + [[package]] name = "indexmap" version = "2.6.0" @@ -1767,18 +2763,75 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "interpolator" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" +[[package]] +name = "interprocess" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f4e4a06d42fab3e85ab1b419ad32b09eab58b901d40c57935ff92db3287a13" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "inventory" version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -1794,6 +2847,43 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.70" @@ -1803,6 +2893,43 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.6.0", + "serde", + "unicode-segmentation", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1812,6 +2939,12 @@ dependencies = [ "spin", ] +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "leptos" version = "0.6.15" @@ -1888,7 +3021,7 @@ dependencies = [ "once_cell", "pad-adapter", "paste", - "rustc-hash", + "rustc-hash 1.1.0", "serde", "serde_json", "server_fn", @@ -1974,7 +3107,7 @@ version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4161acbf80f59219d8d14182371f57302bc7ff81ee41aba8ba1ff7295727f23" dependencies = [ - "base64", + "base64 0.22.1", "cfg-if", "futures", "indexmap", @@ -1982,7 +3115,7 @@ dependencies = [ "oco_ref", "paste", "pin-project", - "rustc-hash", + "rustc-hash 1.1.0", "self_cell", "serde", "serde-wasm-bindgen 0.6.5", @@ -2051,7 +3184,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f204773bab09b150320ea1c83db41dc6ee606a4bc36dc1f43005fe7b58ce06" dependencies = [ "async-trait", - "base64", + "base64 0.22.1", "chumsky", "email-encoding", "email_address", @@ -2075,18 +3208,94 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + [[package]] name = "libc" version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libredox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -2098,6 +3307,25 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", +] + [[package]] name = "linear-map" version = "1.2.0" @@ -2108,6 +3336,12 @@ dependencies = [ "serde_test", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -2136,6 +3370,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "lru" version = "0.11.1" @@ -2183,6 +3426,15 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", +] + [[package]] name = "md-5" version = "0.10.6" @@ -2199,6 +3451,24 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -2221,6 +3491,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -2228,6 +3507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2236,12 +3516,32 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", ] +[[package]] +name = "muda" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8123dfd4996055ac9b15a60ad263b44b01e539007523ad7a4a533a3d93b0591" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "png", + "thiserror", + "windows-sys 0.59.0", +] + [[package]] name = "multer" version = "3.1.0" @@ -2259,6 +3559,54 @@ dependencies = [ "version_check", ] +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.6.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -2275,6 +3623,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2285,6 +3639,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2308,6 +3672,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -2328,6 +3703,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2338,6 +3724,230 @@ dependencies = [ "libm", ] +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.6.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.6.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + [[package]] name = "object" version = "0.36.5" @@ -2369,18 +3979,88 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "open" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f0d54bde9774d3a51dcf281a5def240c71996bc6ca05d2c847ec8b2b216166" +dependencies = [ + "libredox 0.0.2", +] + +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owned_ttf_parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +dependencies = [ + "ttf-parser", +] + [[package]] name = "pad-adapter" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63" +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + [[package]] name = "parking" version = "2.2.1" @@ -2405,7 +4085,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] @@ -2433,6 +4113,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2552,6 +4242,34 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "png" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.0", +] + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "polyval" version = "0.6.2" @@ -2589,6 +4307,34 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.22", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2598,6 +4344,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", + "syn 1.0.109", "version_check", ] @@ -2677,6 +4424,25 @@ dependencies = [ "yansi", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn 2.0.79", +] + [[package]] name = "psm" version = "0.1.23" @@ -2686,6 +4452,78 @@ dependencies = [ "cc", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.0.0", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash 2.0.0", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -2753,13 +4591,114 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f0bfd976333248de2078d350bfdf182ff96e168a24d23d2436cef320dd4bdd" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox 0.1.3", + "thiserror", ] [[package]] @@ -2806,6 +4745,57 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "windows-registry", +] + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + [[package]] name = "ring" version = "0.17.8" @@ -2821,6 +4811,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.6.0", + "serde", + "serde_derive", +] + [[package]] name = "rsa" version = "0.9.6" @@ -2855,6 +4857,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2867,13 +4879,28 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -2942,18 +4969,43 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "self_cell" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + [[package]] name = "send_wrapper" version = "0.6.0" @@ -3160,6 +5212,19 @@ dependencies = [ "digest", ] +[[package]] +name = "sha256" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2", + "tokio", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3194,6 +5259,33 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -3222,6 +5314,40 @@ dependencies = [ "serde", ] +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.6.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix", + "thiserror", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.5.7" @@ -3339,7 +5465,7 @@ checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.5.0", "hex", "once_cell", "proc-macro2", @@ -3364,8 +5490,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a" dependencies = [ "atoi", - "base64", - "bitflags", + "base64 0.22.1", + "bitflags 2.6.0", "byteorder", "bytes", "chrono", @@ -3408,8 +5534,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8" dependencies = [ "atoi", - "base64", - "bitflags", + "base64 0.22.1", + "bitflags 2.6.0", "byteorder", "chrono", "crc", @@ -3485,6 +5611,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + [[package]] name = "stringprep" version = "0.1.5" @@ -3502,6 +5634,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[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" @@ -3553,6 +5691,9 @@ name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3565,6 +5706,25 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.13.0" @@ -3624,6 +5784,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.36" @@ -3655,6 +5826,40 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -3755,7 +5960,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.22.22", ] [[package]] @@ -3767,6 +5972,28 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + [[package]] name = "toml_edit" version = "0.22.22" @@ -3777,7 +6004,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.20", ] [[package]] @@ -3817,7 +6044,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bytes", "futures-util", "http 1.1.0", @@ -3921,6 +6148,38 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tray-icon" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c92af36a182b46206723bdf8a7942e20838cde1cf062e5b97854d57eb01763b" +dependencies = [ + "core-graphics 0.24.0", + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "png", + "thiserror", + "windows-sys 0.59.0", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" + [[package]] name = "typed-builder" version = "0.18.2" @@ -4102,6 +6361,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.10.0" @@ -4113,6 +6378,17 @@ dependencies = [ "serde", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "validator" version = "0.18.1" @@ -4140,6 +6416,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -4156,6 +6438,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4248,6 +6539,115 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d" +dependencies = [ + "bitflags 2.6.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.6.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a94697e66e76c85923b0d28a0c251e8f0666f58fc47d316c0f4da6da75d37cb" +dependencies = [ + "rustix", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5755d77ae9040bb872a25026555ce4cb0ae75fd923e90d25fba07d81057de0" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0a41a6875e585172495f7a96dfa42ca7e0213868f4f15c313f7c33221a7eff" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad87b5fd1b1d3ca2f792df8f686a2a11e3fe1077b71096f7a175ab699f89109" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.70" @@ -4258,6 +6658,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "0.26.6" @@ -4267,16 +6677,28 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "whoami" version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall", + "redox_syscall 0.5.7", "wasite", ] +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -4327,6 +6749,45 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4354,6 +6815,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4385,6 +6861,12 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4397,6 +6879,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4409,6 +6897,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4427,6 +6921,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4439,6 +6939,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4451,6 +6957,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4463,6 +6975,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4475,6 +6993,77 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windres" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82115619221b2b66001a39088b8059d171b1f9005a00d6a10c6e8a71a30a4cdc" +dependencies = [ + "concat-string", + "find-winsdk", +] + +[[package]] +name = "winit" +version = "0.30.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be9e76a1f1077e04a411f0b989cbd3c93339e1771cb41e71ac4aee95bfd2c67" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.6.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "0.6.20" @@ -4484,6 +7073,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" +dependencies = [ + "serde", + "winapi", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -4496,12 +7105,88 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.5", + "once_cell", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.6.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "xxhash-rust" version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "1.0.1" @@ -4601,3 +7286,27 @@ dependencies = [ "quote", "syn 2.0.79", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 77ddef2..4911f52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [".", "avam-wasm"] +members = [".", "avam-client", "avam-wasm"] resolver = "2" [package] @@ -31,7 +31,10 @@ ssr = [ "dep:argon2", "dep:dotenvy", "dep:rand", + "dep:sha256", + "dep:jsonwebtoken", "dep:tokio", + "dep:time", "dep:tracing-subscriber", "dep:leptos_axum", "dep:lettre", @@ -41,6 +44,7 @@ ssr = [ "dep:axum-macros", "dep:axum_session", "dep:axum_session_sqlx", + "dep:tower", "dep:tower-http", "dep:tower-layer", @@ -60,6 +64,7 @@ rand = { version = "0.8.5", optional = true } serde = { version = "1.0.210", features = ["std", "derive"], optional = false } thiserror = { version = "1.0.64", optional = false } tokio = { version = "1.40.0", features = ["full"], optional = true } +time = { version = "0.3.36", optional = true } tracing = { version = "0.1.40", optional = false } tracing-subscriber = { version = "0.3.18", features = [ "env-filter", @@ -107,13 +112,19 @@ tower-layer = { version = "0.3.3", optional = true } http = "1" validator = "0.18.1" +# OAuth2 +base64 = { version = "0.22.1", default-features = false } +sha256 = { version = "1.5.0", optional = true } # this fucker has a dependency on tokio?! +jsonwebtoken = { version = "9.3.0", optional = true } +serde_qs = "0.13.0" + [[workspace.metadata.leptos]] name = "avam" site-root = "target/site" site-pkg-dir = "pkg" style-file = "style/main.scss" assets-dir = "public" -site-addr = "0.0.0.0:3000" +site-addr = "192.168.1.100:3000" reload-port = 3001 browserquery = "defaults" watch = false diff --git a/avam-client/Cargo.toml b/avam-client/Cargo.toml new file mode 100644 index 0000000..603b65f --- /dev/null +++ b/avam-client/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "avam-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.40.0", features = ["full"] } + +tray-icon = "0.19" +image = "0.25" +winit = "0.30" +serde = { version = "1", features = ["derive"] } +config = "0.14.0" +toml = "0.8" + +directories = "5.0" + +anyhow = { version = "1.0" } +thiserror = { version = "1.0" } +winreg = "0.52.0" +interprocess = { version = "2.2.1", features = ["tokio"] } +open = "5.3.0" +clap = { version = "4.5.20", features = ["derive"] } +derive_more = { version = "1.0", features = ["full"] } +uuid = { version = "1.10.0", features = ["fast-rng", "serde", "v4"] } +serde_qs = "0.13.0" +ctrlc = "3.4.5" +reqwest = { version = "0.12.8", default-features = false, features = [ + "rustls-tls", + "json", +] } +rand = "0.8.5" +sha256 = "1.5.0" +base64 = { version = "0.22.1", default-features = false } + +[build-dependencies] +windres = "0.2" diff --git a/avam-client/build.rs b/avam-client/build.rs new file mode 100644 index 0000000..b36f704 --- /dev/null +++ b/avam-client/build.rs @@ -0,0 +1,5 @@ +use windres::Build; + +fn main() { + Build::new().compile("icon.rc").unwrap(); +} diff --git a/avam-client/icon.ico b/avam-client/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9ce992d1bf7a63b873fe86fdc4af81d4e7ef1d76 GIT binary patch literal 15406 zcmeHu2UJ#B(ybGF-o);<6$2pgBL@)?5ETId5fh5EB8mz|z=)!V8I)ue1O;=%jF?aq z6p)~ZB8Z5ff~n0pAp7mQj>EOJ?~%ZUvt)~g?xPXo;v4L)v4OMY|`XsO@7hj z%P*Vor&g2EziiUPrb&|~T3R1}H~gwelT?0YY5C{BtC}>)Yu2QRA-}^%_{7K0`Qyio zpYhB8hw=@DZ)@^9ihTBu#?Sc$Gk16Qs0EP=@}d_;-iTT-|JKS?%g;s3o|#}^pzFxL zfAKvp`K_U$;iQ<@=)1vVN20Bf9!3oxf<=*YF?&WB^73+V>eR`%(b0=G@cHI{^NwHJ z*w`#cNLcxHNn8vD5Aed$#j)tq!xg%%n`6j8Z}jTk4YOv>z?6w0NKIXHoX^YlDC19m z`jZ!Rd}*ey1?`sKV14QutXL9_i0Pr|*xnK`QS)J>-5Q1zdH*19FRWRUw1^MOp8PR% zYUmjc=g!bmZ;laugAg1r05(P~F>Z)ER>g*5-_CXD?qmmBOA{Brc0Z2O~8sjh>6x8G|sMYz&U>PeJ9y984SMg(X3{h@U+M=L=6`WFHll z%^!zv`}M-iu&Kz;Jw3QFpTj zhjt|)bMrWCj`hXnHA~@OZH|yJfjDv`qaW9=f9O!ggQ)rQka^?~{CxUgZ`xK=T`5J? z<+C_{A{KY+s-yE`-mMD)hxs8p`(zlO{WLT-ZQ58Yb`rN}0c_fu zU_)v$Hm_fc9cyC|H{Az2HYH=wfIb+`d9qGqrt!hwe0o@Zavu!%j;w;b+{Tf#+qFrn z;Mmay;bEaTd?+2;wrs@OOIpa1;lvkVOlHu?ArI5B>FNWs*pQ;H@} zoRB-nchFv4UEOGY$B)vUB4;f6{yXEheAtQ-%6M>i&fGb5nI|&e6%`j^@19*)la$DK zxEKj5mf_g3!?;*>0hz~-K964#Th_I!V ztX#Pq1H5{|NUJs4H2)G-M*0Y!IvIO*ZO6sZ3n(fqLgm#fsH`kU+2xDawJYt8n_IW# zd`~;dU->0}|Gpt}>r!S87%-qRJ16VIs+G%>7_HsnYiOx`34M(gn8Q1x_*@~@Bqzex zryopo+rZk?5a%yk!0N=+sI0n*ty?zZ-1(vpQBjenzxd({cfM2h^M{5q_s@Rzvqj-E z!|$FiIft|PXW-MP2XtFD<=kJRy}1eFMh5on-vd`i2k5qH1|5oai>8=9Wg?P!CffWx zjQf&v`ZRj?aF@7*9Xq#IeD&2=9-RD#_Cd}nLgvmZ$b*NABSsGQ$C0B)D2Fj+LJ$fI z@(~vk1q00%(C59Tudb%tr8=#ebDm?o1N%akpNkHuscRM6Fz3B?`0zn&-L~bzFMs*V z4xHof*@NtjZ~y-NuN0RQqq}n#Wo>fr%DaXQ9*FDLt5J3BDi+S0gRb`NmA~t0sPTSP zgJ(A<aUahNwd940!g zVa$BMpiN8Ki5lW#7Gd?O<;r_{c6Sy1-#I&X9nSfF{h0#d-;<(aq91Npw?>&)_Equ* zJM-Or0cqV2rr`ohW1-ZFrqvv zW4WiZFtE2H()aGflgAJ7;X?z8&z?pvXG?_i*TVAg23R@K5TiU>WA>EMXl!i2l(Bvo z=+YWP-L)`~=X31fPD}|IhfyPjVtvY5#>WcEZ{HlWeo+B)reO$~Fs7RtWsX|$yM zw!#+XIZGFY!^hneDa$5f)5>w!kr;rd4{zhr#S-*(HenvAiG_ih@atiX=g%JF;di%D zRdEhs!#gVH;_t4F3+Hk%uwPG@8|c7PUmHE$Tu@(k^RCP-;^5%0=gO4|CC3(jR&j0> z{ilL<6EM(?xnmoA>!yuS{WLMnuNBtCc;Wrqm#C~L!yq?3^tM++SxEsJ(SWn3k0I3G z0<-+JG0j(nQM8ZycW$9`2P?ES(TBOAF1-8n#Ql5qpU=Om@@l2xqq25Yo7S{pJ+9?0 zdUevLjWgHot-|<$hKLw$gy=E)NK2ZG#)h{jJzt2hkfCUN|AuGt9!ZOWkiBaLR!`DL z+!zytkFZ5;^;L9WZrskyP|+c1R3L8G-Kyf+<&IOM@1@k;u7@#wR*!d!0q1w^Y>(^L zDwVw1muno^R}Vqnx){PU?cSj^I+(UX(u!z+b7p5{V(W%PJbzG)vZBM7KcY1fCTL;J z6k}{!I*EDnHgsfeZOi<%y@e4L$1FryS?Mv(Epg`$yl>`|U%B$$QDQ3Nj?CZ3-36B~ zmB7_T1Kn&mpK&wBlcvmhvFL=>IPRrXkH{G&AD<;$mdaKD>UF;NEt^fXa6%_MxqF%W@B!&*i8vTzf`myb6rIV$0@iQg z(KvgRyj?C3eKSVcUFT?2BuU?DhTd`q^DHynl<>(K@ViMxnSMmvxOJ z+*yOVIXNgX_~C>5Z~XoJyKzp5y`P5s+d3#HDC=tF6{QXp|0~Enjd7z!AYkYq>}7p* zr~VdpZry~bV+LY~n?6DZ>LGcm1$IPQBYm+Q4z6@V>OwyR4sb->jqAJvywSteQK_vu z*;?Za&*A9N!wH;A?Cx)e-2Xm%_Ux(7ICuam=6Dg)rXZWSiz)Aswx;^%Y;TQu5n-sk zevPs40S+`VSJ1F(_%4LhOm-((^$M$@;9h^GZq9-wo^0JF~{`@KG zZr#8_*8ROax+#8CdZDDTq@?(efq|C6KbZTIE}}zI+M{=Ja#D6(-K~Z@^>w(&c;Cm< zoolZ{H|CwzmZorZazy03xv0ESjw6TD;WyA5;5$N=RYM+HFN3ObxQjF{dv!xJ$c>G@E$ch`y|3x?nSMwhm;!|SdQa4BqCH^gTFCzaIOiOYK`OZ)2e|+7) zHN<~KN5Li~?=_+bF42+F<?m{p%^; zeDh7Lj+WM10|V7o3p2Cb78Vv1^E5LPlTF00SFo0j`0a0hJA&W$pa_N}YyKnU|HoQH zZ;9J7r|hr)FMjchg@%TzU4BFS3a8JQer?sNmG^h=+VPzE$eY~U(+y|y^BVH=a~n>b z%6glA@WAu+8&dC(b0`lPACl$L%_W7{$rQe)H^rPH`ysaXpEwiQAE^t)4n|XwEiEnc zV`8H39y)yJ^_jff55>fV^3LXA0}!gvIa48+)g;g}o}j6~uM7cQJfS@}hj zl%8wI%Rlq_;GzBBO`JGBw{`2*aeSu_Mb1<3O0mnIV#r>It>{v`zWCx-OU=#A^A|-g zdVIF1pi%Tabo4NyS!4J0><*QB3ura}O2P4rwbfZ4v_tUd044uFS5$yYWtVV?cl_0= zN@98y%-t^G^5s%w=bU^M8alZ^rBW>*rsm4Ige&=XZA$D#_CowIm}k9b)TqE42Qv=5 zEg;spIw=7ohWMhLsR1##uM`YWa6rMG2C$Y)SiS@WtW5+D+|Ar9E_xx)WCA7y2jk+U zGF+~p{*|(hD-cZY{P}a(e_-GJu|Z?@eEH>P*pui^wy%i;We&NIGtAqb z<>jAl%*faeA96b8kk6f?-@p17Arg`erDdJqSn0w`+sJP(G%2g{a{O)(Z3+CLCYsvZlm^H|Er4H)M z8Z^3{b-N4Oc5HjYTnguk&tmHMv9MHWL66wyC*8GMHdE|}oFC3p?~NPR!KtIIvOWa| zRo2;5!LU+@b>*MUhX=7iZQ_%YCWPPuv8?1Z$#7zQy?yIu+E^*}?Au+_u3fvS)I(zO zKg)@T?!TwlQuin)XQzt?4jy=y&wW`;%#XPs&Jrsn&V+Fz16co?&~CmVcj=F8=EvEr zg+j)TQus^-MjFrAN| zH)9=VghS->1V`@a?hHe&X?DbP#;QvnHf`B>@mIh4mBc2&p~bGhZ^+*ywhwFBvPEX{ z+T<4{#0uumoke|!{fZ4x1h*ADXH4KQ)}Gm@t-X$F)(`7b*1)Z^9rs?By8jtxm7jms z(^Ovv$*e01=^N&TI>Zf$L417d5%R`P z)J<&T`-a?uuC$x=p`lamGbS{~5tA__KB-R|_zUjLx!Q0I*5=08otDP9QB0p*MI8II z5+8T%+>VH_Da6y96}(LFS~&-&jyB{Ca#^275VL6oV`8v|e^~?1hjWl;#yQ3-!F6tWA5}IoXO54&T@^l^D{<{gVk=WV`b=vzbSGwX{ra^=auubfrlx(VqwMqN zLwr`^d{5>tdv~Vod`UhP1N-+@bQa8A;WK0n+!KAibK?-6^kR$$w~`$#NP!2 zZ*SNPHk9CD9?00Uf%WJ);@a0)3tv|7<~_T1D)_OvN>kC_R9BsA(SQlpA^eps_v-*- zg!s5FZB(Z|~<#C}Bo?&RVNSRyo*GQFzDAF(Ht^`Eg`S;SV)VCmSUhhm z4(>`p*~LNyV>y|56!GM}?c13!CbUp`9i-nuxCL|lHi%s~AGe7KIXT#I4XqUYJKNdt zF3iKplP4}S=juv*rDpv!#nCpDQ=ZuN@7GlA)A?WX<4j-2`7}%{1{CgT8#LpHh76hOq zFN6HZZnQI0NB1t)6$wIvCYg7bALVp=(<;_<6ZtxR(vG zGxy^D-8uz(6;4Sw3ekV%(s-T?d9OdOL3$Bth&iVp*n61z%AF=QBqA8l{J_A#Tf&d# zo<4%_gX1q9eJx7_Wnq1B=?z!9VK{_vFzd8A>d?Neu5g<6_vv0629pM#w0C9Az%|g7}``*)Lu^L;UlNwVd;1yg!_?Wu~kFV%t^$>-;+nU0DQM^&!MkNTcoU7fTxe{;^F<9$CX8w$=}eYHa2I)-@YBY-_mBYJQiH*HKE=}ElvIu^~F z&Arr8_(t2dX6zO6R&tdG-`#t?W!siH)LUY{$RMvieG0GF)H3Ioq3pflv&7pa1`B8F z$-GAR!lMUw!O=nuUQW6Q_On4)U^~nYw!-2mZIQlVGG4uSgx9ZMVavu8bhFdJOuv>$ znxc&fUM($b>8Rtd-v~Obe-IUaQ(uI>+-%A-cIgC1Ky9pBZn*Z z(1M5w=w_`BPX|qS^Q?z(kH>kp!qma!jRQ4Ml(mjr!E1$2T$8W_{Twwgu2(a7*fc{z z{9JziPT|w;)YoF9k3ALy+0f3kFx^)dlj-MU$+;%SPUcxYL@&3l9*9=|5f^Vj-&dlywxf2mm|PZYjOY)A4aLrNt165(Bj^>b2qzkb|*Uw0Kout#m& z0DVmH)4{9}nuwohiqgCt3g6Mt@D3Z-E+bFX22;lk#!K>$pE#n6#o5To+>KQc-4HuQ z8&M+-Fx%e<6Zx)P>tgZUz1s>WWo>4_+*GBk!IXA9f7VRe=(AVMyCS&%V*6r0-wyEh zE~u%kRX827eaSENdH0F_raZUA6-)WfdU)9B!K;fl2D<8FSTE7t08<7Fe`tcJ(Z=Ku z4G=TI9;IiG(x>0imR?|4%p9zU4#x+^8~IH5#Hj)9NJ|)nU9nDBH%$+T^zrzyrid73 zj+|pV$&VaE2l9G0gup? z{%j@Rm7G`fm)x^0`)RgqT1VdX0^B=j@Ltu1Uw1w7*!l?Z(Z^)Q_6gj#k)AqSn+7^t zwZ_1{%)`nrko$Va`1A~6<40o~xyU!<T6ryC+^k_jTm zc0|q9QpB>4Q@BYxD^rCdwYD@ON2)^3$rI!=p4<-&4edj{CH4!p=fu2i*Y(<(#>5rO zdzgzT`m?V!|(9^{X4vS_Xfp9 zxwv=xrozQO`0hR;XHLNQ{+iemse_XJ!^jQR;mN~0sJ?O@+fw6@d+G?zunz6c99y{A zF1A+aOm0io5HNHwx$k>8m7TSbIt#ucxzEojQWMNhNlm>g_gxA5B<4nhlZWa=zfmdu zPY2j{#+~`-pa@8m;4U>qnh_rd~_7NdwY`O?1Ii*gH2mg z-YYSD&x`i~0mFT%?*4`TcpV@B5nTRqUgE%szr*_U&%K{5i8A_}A6S z@VqUbY#CkYEDkh)051BY={92@g0aQn>TaMm`^=?px8wZ7YA~sQ<)Q; zz*hQn+`?H1pWu(>F|*m9aS_u;yI^^Q2OiwHj>MQTNS&#H-LWP(vZ^Dh3f6&(P|l(L zRy8(7_r{@EQ>2C&VsXT9#{37&59gydc}HLNJWmQ4%ldSy!acrZPXP0jSM%n~8BbjW z>y~~0G{g@ySX+j~#>SMfC#G?z?;ze`EuhCctev?Ld#pA~+{Mk>Dr6o_$Kv=!=*Au< z8*>#pS+s#~4_lni&qVTqp@<7M#lAHG*tV!M`*|79qb-rX#0JNcX)9Nbu{GkYa=+ys zh(W?cUB)mA?4}R`12iBERAKJ@65WX>&M;zm(J*9XRY*| z3uk=tWY!ifErqN8d)ULLH3)WP&N?_^`}XZM_wU_n3>@LF{!@vc@Qm^9?Q6W`eRV8jANzMku)oI!q2HPydXy-e?T;9|8m3X`_b^#j9seApK|(S0yizd(oD6q+L5xj1Bb1qX+j*+7oSi5gQ{G5ph8q^iTJWbHw#R!q%lh~6`h21;1!l$nX_3x(iCCC{#b+X5V z;BnGF*dYB0Lx=kIpiYwK{5`wj$A2}%j=pA%-_6O%DTRF_PgpZ!Bzw%H@9!l0fn}dv z9XnBf1NJVM@NW5dN3JI(`uNdz=-gPAelenAU;G)+*JLJ2<<#^cI+NNBq zy85WTuC8$$b5}27eA4@odE_wf6bl94vgO?(|6in@K9J1%TY4~@=^x!3?dgLZ;YJxd zY8da7Q_M@>jtE4--1`P@eS)z(2@ObW(KLP;d50B!KZO0qM;<}wG$ff!}OQ;zEDs`KnYnL=Jwa!Rq6ABvIu z^S?CYopKf``nIcwhev2)V&eAGxu;8R+`RGKn>Vi;J`huo+)e7P>T1TF^aBc>yoNm4 zPTE{Hb9d%U4a|QYT&^f9K6&EA=GfTS5FH&Ixl_efC4T=Dd(PMV-68K5zYx5!74JGr z;z*v(&dy_p4<8<#kgy^xCnvjzI7e-LecioVw{G04tGjux?q=;R+D86X{s*e?@aZ#& z{|%x~IW%wHT!-%wo=Wn8|D@i0{r@v$J~, + receiver: Receiver, + items: HashMap<&'static str, MenuId>, +} + +impl App { + pub fn new( + config: Config, + sender: Sender, + receiver: Receiver, + ) -> Result { + let mut tray_icon = TrayIcon::new(load_icon(), crate::PROJECT_NAME); + + let login = MenuItem::new("Login", true, None); + let forget = MenuItem::new("Logout and Exit", false, None); + let quit = MenuItem::new("Exit", true, None); + + let mut items = HashMap::new(); + + let c = config.clone(); + let login_id = tray_icon.add_menu_item(&login, move |item| { + // .. + let item = item.as_menuitem().unwrap(); + if item.text() != "Login" { + open::that(BASE_URL)?; + return Ok(()); + } + + oauth::open_browser( + c.code_verifier().unwrap(), + c.code_challenge_method().unwrap(), + )?; + + Ok(()) + })?; + + tray_icon.add_seperator()?; + + let c = config.clone(); + let s = sender.clone(); + let forget_id = tray_icon.add_menu_item(&forget, move |_| { + c.set_token(None)?; + s.send(Event::Quit)?; + + Ok(()) + })?; + + let s = sender.clone(); + tray_icon.add_menu_item(&quit, move |_| { + s.send(Event::Quit)?; + Ok(()) + })?; + + items.insert("login", login_id); + items.insert("forget", forget_id); + + Ok(Self { + config, + tray_icon, + sender, + receiver, + items, + }) + } +} + +impl ApplicationHandler for App { + fn new_events(&mut self, event_loop: &ActiveEventLoop, _: StartCause) { + event_loop.set_control_flow(ControlFlow::WaitUntil( + std::time::Instant::now() + std::time::Duration::from_millis(16), + )); + + if let Ok(event) = tray_icon::menu::MenuEvent::receiver().try_recv() { + self.tray_icon.handle(event.id()); + } + + if let Ok(event) = self.receiver.try_recv() { + match event { + Event::Quit => { + println!("Shutting down EventLoop"); + event_loop.exit() + } + Event::TokenReceived { .. } => { + self.tray_icon + .set_text(self.items.get("login").unwrap(), "Open Avam") + .unwrap(); + + self.tray_icon + .set_enabled(self.items.get("forget").unwrap(), true) + .unwrap(); + } + _ => {} + } + } + } + + fn resumed(&mut self, _: &winit::event_loop::ActiveEventLoop) { + let _ = self.tray_icon.build(); + + let _ = self.sender.send(Event::Ready { + config: self.config.clone(), + }); + } + + fn window_event( + &mut self, + _: &winit::event_loop::ActiveEventLoop, + _: winit::window::WindowId, + _: winit::event::WindowEvent, + ) { + // We don't have a window + } +} + +fn load_icon() -> tray_icon::Icon { + let icon = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../public/favicon-32x32.png" + )); + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(icon) + .expect("Failed to open icon path") + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") +} diff --git a/avam-client/src/config.rs b/avam-client/src/config.rs new file mode 100644 index 0000000..ee31469 --- /dev/null +++ b/avam-client/src/config.rs @@ -0,0 +1,125 @@ +use std::{ + io::Write, + sync::{Arc, RwLock}, +}; + +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::{ + dirs::{Dirs, DirsError}, + models::{CodeChallengeMethod, CodeVerifier}, +}; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(with = "arc_rwlock_serde")] + token: Arc>>, + #[serde(skip)] + code_verifier: Arc>>, + #[serde(skip)] + code_challenge_method: Arc>>, +} + +impl PartialEq for Config { + fn eq(&self, _: &Self) -> bool { + true + } +} + +#[derive(Debug, Error)] +pub enum ConfigError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Dirs(#[from] DirsError), + #[error(transparent)] + Toml(#[from] toml::ser::Error), + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +impl Config { + pub fn set_token(&self, token: Option) -> Result<(), ConfigError> { + *self.token.write().unwrap() = token; + self.write()?; + Ok(()) + } + + pub fn token(&self) -> Option { + self.token.read().unwrap().clone() + } +} + +impl Config { + pub fn set_code_verifier( + &self, + code_verifier: Option, + ) -> Result<(), ConfigError> { + *self.code_verifier.write().unwrap() = code_verifier; + Ok(()) + } + + pub fn set_code_challenge_method( + &self, + code_challenge_method: Option, + ) -> Result<(), ConfigError> { + *self.code_challenge_method.write().unwrap() = code_challenge_method; + Ok(()) + } + + pub fn code_verifier(&self) -> Option { + self.code_verifier.read().unwrap().clone() + } + + pub fn code_challenge_method(&self) -> Option { + self.code_challenge_method.read().unwrap().clone() + } +} + +impl Config { + pub(crate) fn new() -> Result { + let config = config::Config::builder() + .add_source( + config::File::with_name(Dirs::get_config_file()?.to_str().unwrap()).required(false), + ) + .build() + .unwrap(); + + let config: Self = config.try_deserialize().unwrap_or_default(); + + config.write()?; + + Ok(config) + } + + fn write(&self) -> Result<(), ConfigError> { + let toml = toml::to_string_pretty(self)?; + let mut file = std::fs::File::create(&Dirs::get_config_file()?)?; + file.write_all(toml.as_bytes())?; + Ok(()) + } +} + +mod arc_rwlock_serde { + use serde::de::Deserializer; + use serde::ser::Serializer; + use serde::{Deserialize, Serialize}; + use std::sync::{Arc, RwLock}; + + pub fn serialize(val: &Arc>, s: S) -> Result + where + S: Serializer, + T: Serialize, + { + T::serialize(&*val.read().unwrap(), s) + } + + pub fn deserialize<'de, D, T>(d: D) -> Result>, D::Error> + where + D: Deserializer<'de>, + T: Deserialize<'de>, + { + Ok(Arc::new(RwLock::new(T::deserialize(d)?))) + } +} diff --git a/avam-client/src/dirs.rs b/avam-client/src/dirs.rs new file mode 100644 index 0000000..ff1d8d6 --- /dev/null +++ b/avam-client/src/dirs.rs @@ -0,0 +1,43 @@ +use std::path::PathBuf; + +use directories::ProjectDirs; +use thiserror::Error; + +pub struct Dirs {} + +#[derive(Debug, Error)] +pub enum DirsError { + #[error("Unable to get Project Directories")] + ProjectDirs, + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +impl Dirs { + pub fn get_config_dir() -> Result { + let Some(proj_dirs) = ProjectDirs::from("nl", "Avii", "Avam") else { + return Err(DirsError::ProjectDirs); + }; + + let config_dir = proj_dirs.config_local_dir(); + + if !config_dir.exists() { + std::fs::create_dir_all(config_dir)?; + } + Ok(config_dir.to_path_buf()) + } + + pub fn get_config_file() -> Result { + let mut c = Self::get_config_dir()?; + c.push("config.toml"); + Ok(c) + } + + pub fn get_lock_file() -> Result { + let mut c = Self::get_config_dir()?; + c.push(".lock"); + Ok(c) + } +} diff --git a/avam-client/src/icon.rs b/avam-client/src/icon.rs new file mode 100644 index 0000000..61385b6 --- /dev/null +++ b/avam-client/src/icon.rs @@ -0,0 +1,117 @@ +use std::collections::HashMap; + +use tray_icon::{ + menu::{IsMenuItem, Menu, MenuId, MenuItemKind, PredefinedMenuItem}, + TrayIconBuilder, +}; + +// The app winit thing can listen in on the same events +// coming from the state machine to give feedback to the user +// changing the icon, the alt texts, and the context menu +// based on the last received event + +type Callback = Box Result<(), anyhow::Error> + 'static>; + +pub struct TrayIcon { + icon: tray_icon::Icon, + title: String, + menu_items: HashMap, + menu: Menu, + tray_icon: Option, +} + +#[allow(unused)] +impl TrayIcon { + pub fn new(icon: tray_icon::Icon, title: &str) -> Self { + Self { + icon, + title: title.to_string(), + menu_items: HashMap::new(), + menu: Menu::new(), + tray_icon: None, + } + } + + pub fn add_seperator(&mut self) -> Result<(), anyhow::Error> { + self.menu.append(&PredefinedMenuItem::separator())?; + Ok(()) + } + + pub fn add_menu_item( + &mut self, + item: &dyn IsMenuItem, + callback: F, + ) -> Result + where + F: Fn(&MenuItemKind) -> Result<(), anyhow::Error> + 'static, + { + let id = item.id().clone(); + self.menu.append(item)?; + + self.menu_items + .insert(id.clone(), Box::new(callback) as Callback); + + Ok(id) + } + + pub fn set_enabled(&self, id: &MenuId, enabled: bool) -> Result<(), anyhow::Error> { + if let Some(item) = self.menu.items().iter().find(|i| i.id() == id) { + if let Some(menuitem) = item.as_menuitem() { + menuitem.set_enabled(enabled); + } + if let Some(menuitem) = item.as_check_menuitem() { + menuitem.set_enabled(enabled); + } + if let Some(menuitem) = item.as_icon_menuitem() { + menuitem.set_enabled(enabled); + } + if let Some(menuitem) = item.as_submenu() { + menuitem.set_enabled(enabled); + } + } + Ok(()) + } + + pub fn set_text(&self, id: &MenuId, text: &str) -> Result<(), anyhow::Error> { + if let Some(item) = self.menu.items().iter().find(|i| i.id() == id) { + if let Some(menuitem) = item.as_menuitem() { + menuitem.set_text(text); + } + if let Some(menuitem) = item.as_check_menuitem() { + menuitem.set_text(text); + } + if let Some(menuitem) = item.as_icon_menuitem() { + menuitem.set_text(text); + } + if let Some(menuitem) = item.as_predefined_menuitem() { + menuitem.set_text(text); + } + if let Some(menuitem) = item.as_submenu() { + menuitem.set_text(text); + } + } + Ok(()) + } + + pub fn handle(&self, id: &MenuId) { + if let Some(item) = self.menu_items.get(id) { + if let Some(i) = self.menu.items().iter().find(|i| i.id() == id) { + if let Err(e) = item(i) { + eprintln!("{:#?}", e); + } + } + } + } + + pub fn build(&mut self) -> Result<(), anyhow::Error> { + self.tray_icon = Some( + TrayIconBuilder::new() + .with_tooltip(self.title.clone()) + .with_menu(Box::new(self.menu.clone())) + .with_icon(self.icon.clone()) + .build()?, + ); + + Ok(()) + } +} diff --git a/avam-client/src/lock.rs b/avam-client/src/lock.rs new file mode 100644 index 0000000..6672701 --- /dev/null +++ b/avam-client/src/lock.rs @@ -0,0 +1,48 @@ +use std::{fs, path::PathBuf}; + +use crate::dirs::{Dirs, DirsError}; +use thiserror::Error; + +pub struct Lock { + force: bool, + lock_file: PathBuf, +} + +#[derive(Debug, Error)] +pub enum LockError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Dirs(#[from] DirsError), + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +impl Lock { + pub fn new(force: bool) -> Result { + let lock_file = Dirs::get_lock_file()?; + + Ok(Self { lock_file, force }) + } + + pub fn lock() { + if let Ok(lock_file) = Dirs::get_lock_file() { + if !fs::exists(&lock_file).unwrap() { + fs::write(&lock_file, "").expect("there to not be issues with the filesystem"); + } + } + } + + pub fn unlock() { + if let Ok(lock_file) = Dirs::get_lock_file() { + if fs::exists(&lock_file).unwrap() { + let _ = fs::remove_file(lock_file); + } + } + } + + pub fn is_locked(&self) -> bool { + !self.force + && fs::exists(&self.lock_file).expect("there to not be issues with the filesystem") + } +} diff --git a/avam-client/src/main.rs b/avam-client/src/main.rs new file mode 100644 index 0000000..25a2aee --- /dev/null +++ b/avam-client/src/main.rs @@ -0,0 +1,180 @@ +#![allow(clippy::needless_return)] + +mod app; +mod config; +mod dirs; +mod icon; +mod lock; +mod models; +mod oauth; +mod pipe; +mod state_machine; + +use crate::config::Config; +use clap::Parser; +use lock::Lock; +use oauth::{start_code_listener, start_code_to_token}; +use pipe::Pipe; +use state_machine::Event; +use tokio::task::JoinSet; + +pub static BASE_URL: &str = "https://avam.avii.nl"; +pub static PROJECT_NAME: &str = "Avii's Virtual Airline Manager"; +pub static COPYRIGHT: &str = "Avii's Virtual Airline Manager © 2024"; +pub static CLIENT_ID: uuid::Uuid = uuid::uuid!("f9525060-0a34-4233-87e2-0f9990b7c6db"); +pub static REDIRECT_URI: &str = "avam:token"; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct Arguments { + #[arg(short, long)] + code: Option, + #[arg(short, long, action)] + force: bool, +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { + let (event_sender, event_receiver) = tokio::sync::broadcast::channel(1); + let args = Arguments::parse(); + + if handle_single_instance(&args).await? { + return Ok(()); + } + + register_url_scheme()?; + + let config = Config::new()?; + + let mut futures = JoinSet::new(); + + // Register Quit handler + let sender = event_sender.clone(); + let mut ctrl_c_counter = 0; + ctrlc::set_handler(move || { + println!("CTRL_C: Quit singal sent"); + ctrl_c_counter += 1; + if ctrl_c_counter >= 3 { + let _ = unregister_url_scheme(); + Lock::unlock(); + std::process::exit(1); + } + + if let Err(e) = sender.send(Event::Quit) { + println!("{:#?}", e) + }; + })?; + + // Start the State Machine + let c = config.clone(); + let sender = event_sender.clone(); + let receiver = event_receiver.resubscribe(); + futures.spawn(state_machine::start(c, sender, receiver)); + + // // Start the code listener + let receiver = event_receiver.resubscribe(); + let (pipe_sender, pipe_receiver) = tokio::sync::broadcast::channel(100); + futures.spawn(start_code_listener(pipe_sender, receiver)); + + // Start token listener + let c = config.clone(); + let sender = event_sender.clone(); + let receiver = event_receiver.resubscribe(); + futures.spawn(start_code_to_token(c, pipe_receiver, sender, receiver)); + + // Start the Tray Icon + let c = config.clone(); + let sender = event_sender.clone(); + let receiver = event_receiver.resubscribe(); + start_tray_icon(c, sender, receiver).await?; + + // Wait for everything to finish + while let Some(result) = futures.join_next().await { + if let Ok(Err(e)) = result { + panic!("{:#?}", e); + } + } + + // Cleanup + unregister_url_scheme()?; + Lock::unlock(); + + Ok(()) +} + +use app::App; +use tokio::sync::broadcast::{Receiver, Sender}; +use winit::event_loop::EventLoop; +async fn start_tray_icon( + config: Config, + sender: Sender, + receiver: Receiver, +) -> Result<(), anyhow::Error> { + let mut app = App::new(config, sender, receiver)?; + let event_loop = EventLoop::new()?; + + event_loop.run_app(&mut app)?; + println!("EventLoop Shutdonw"); + + Ok(()) +} + +// +// +// +// +// +// +// +// +// +// +// +// + +/// returns `Ok(true)` if we need to quit gracefully +/// returns `Err(e)` if we're already running +async fn handle_single_instance(args: &Arguments) -> Result { + let lock = Lock::new(args.force)?; + + if lock.is_locked() && args.code.is_none() { + return Err(anyhow::anyhow!("Lockfile exists, exiting.")); + } + + if let Some(code) = &args.code { + Pipe::send(code).await?; + return Ok(true); + } + + Lock::lock(); + Ok(false) +} + +fn register_url_scheme() -> Result<(), anyhow::Error> { + use winreg::enums::*; + use winreg::RegKey; + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + let avam_schema_root = hkcu.create_subkey("Software\\Classes\\avam")?; + avam_schema_root.0.set_value("URL Protocol", &"")?; + let command = avam_schema_root.0.create_subkey("shell\\open\\command")?; + + let current_exec = std::env::current_exe()?; + + command.0.set_value( + "", + &format!("\"{}\" -c \"%1\"", current_exec.to_str().unwrap()), + )?; + + Ok(()) +} + +fn unregister_url_scheme() -> Result<(), anyhow::Error> { + use winreg::enums::*; + use winreg::RegKey; + + let hkcu = RegKey::predef(HKEY_CURRENT_USER); + hkcu.delete_subkey_all("Software\\Classes\\avam").ok(); + + Ok(()) +} diff --git a/avam-client/src/models.rs b/avam-client/src/models.rs new file mode 100644 index 0000000..0d83cf2 --- /dev/null +++ b/avam-client/src/models.rs @@ -0,0 +1,300 @@ +use std::str::FromStr; + +use derive_more::{derive::From, Display}; +use rand::{distributions::Alphanumeric, Rng}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::REDIRECT_URI; + +#[derive( + Clone, Debug, Display, From, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub struct RedirectUri(String); + +impl RedirectUri { + pub fn new(uri: &str) -> Self { + Self(uri.to_string()) + } +} + +impl From<&str> for RedirectUri { + fn from(value: &str) -> Self { + Self::new(value) + } +} + +#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(from = "String")] +pub struct CodeVerifier(String); + +impl From for CodeVerifier { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for CodeVerifier { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +impl CodeVerifier { + pub fn new() -> Self { + let token: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(120) + .map(char::from) + .collect(); + + Self(token) + } +} + +impl Default for CodeVerifier { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Error)] +pub enum CodeChallengeMethodError { + #[error("Code challenge method is not valid.")] + Invalid, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum CodeChallengeMethod { + #[serde(rename = "plain")] + Plain, + #[serde(rename = "S256")] + Sha256, +} + +impl FromStr for CodeChallengeMethod { + type Err = CodeChallengeMethodError; + + fn from_str(s: &str) -> Result { + match s { + "plain" => Ok(Self::Plain), + "S256" => Ok(Self::Sha256), + _ => Err(CodeChallengeMethodError::Invalid), + } + } +} + +#[derive(Debug, Error)] +pub enum ResponseTypeError { + #[error("The response type is not valid.")] + Invalid, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum ResponseType { + #[serde(rename = "code")] + Code, +} + +impl Display for ResponseType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + ResponseType::Code => "code", + } + ) + } +} + +impl FromStr for ResponseType { + type Err = ResponseTypeError; + + fn from_str(s: &str) -> Result { + match s { + "code" => Ok(Self::Code), + _ => Err(ResponseTypeError::Invalid), + } + } +} + +#[derive(Debug, Error)] +pub enum GrantTypeError { + #[error("The grant type is not valid.")] + Invalid, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum GrantType { + #[serde(rename = "authorization_code")] + AuthorizationCode, +} + +impl Display for GrantType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + GrantType::AuthorizationCode => "authorization_code", + } + ) + } +} + +impl FromStr for GrantType { + type Err = GrantTypeError; + + fn from_str(s: &str) -> Result { + match s { + "authorization_code" => Ok(Self::AuthorizationCode), + _ => Err(GrantTypeError::Invalid), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthorizeRequest { + client_id: uuid::Uuid, + response_type: ResponseType, // Make type (enum:code,) + state: Option, // random string for CSRF protection + code_challenge: String, // pkce + code_challenge_method: Option, // Make type (enum:sha256,) hashing algo + redirect_uri: RedirectUri, // Make type + scope: Option, // space seperated string with permissions +} + +impl AuthorizeRequest { + pub fn new( + client_id: uuid::Uuid, + response_type: ResponseType, + state: Option, + code_challenge: String, + code_challenge_method: Option, + redirect_uri: RedirectUri, + scope: Option, + ) -> Self { + Self { + client_id, + response_type, + state, + code_challenge, + code_challenge_method, + redirect_uri, + scope, + } + } + + // pub fn client_id(&self) -> uuid::Uuid { + // self.client_id + // } + + // pub fn response_type(&self) -> ResponseType { + // self.response_type.clone() + // } + + // pub fn state(&self) -> Option { + // self.state.clone() + // } + + // pub fn code_challenge(&self) -> String { + // self.code_challenge.clone() + // } + + // pub fn code_challenge_method(&self) -> Option { + // self.code_challenge_method.clone() + // } + + // pub fn redirect_uri(&self) -> String { + // self.redirect_uri.clone() + // } + + // pub fn scope(&self) -> Option { + // self.scope.clone() + // } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthorizationResponse { + code: String, + state: Option, +} + +impl TryFrom for AuthorizationResponse { + type Error = anyhow::Error; + + fn try_from(s: String) -> Result { + let mut parts = s.split('?'); + let Some(protocol) = parts.next() else { + return Err(anyhow::anyhow!("Invalid AuthorizationResponse")); + }; + + if protocol != REDIRECT_URI { + return Err(anyhow::anyhow!("Invalid AuthorizationResponse")); + } + + let Some(qs) = parts.next() else { + return Err(anyhow::anyhow!("Invalid AuthorizationResponse")); + }; + + if parts.count() > 0 { + return Err(anyhow::anyhow!("Invalid AuthorizationResponse")); + } + + Ok(serde_qs::from_str(qs)?) + } +} + +impl AuthorizationResponse { + pub fn code(&self) -> String { + self.code.clone() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthorizationCodeRequest { + grant_type: GrantType, + code: String, + redirect_uri: RedirectUri, + client_id: uuid::Uuid, + code_verifier: CodeVerifier, +} + +impl AuthorizationCodeRequest { + pub fn new( + grant_type: GrantType, + code: String, + redirect_uri: RedirectUri, + client_id: uuid::Uuid, + code_verifier: CodeVerifier, + ) -> Self { + Self { + grant_type, + code, + redirect_uri, + client_id, + code_verifier, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthorizationCodeResponse { + token: String, +} + +impl AuthorizationCodeResponse { + pub fn token(&self) -> String { + self.token.clone() + } +} diff --git a/avam-client/src/oauth.rs b/avam-client/src/oauth.rs new file mode 100644 index 0000000..4285829 --- /dev/null +++ b/avam-client/src/oauth.rs @@ -0,0 +1,95 @@ +use std::time::Duration; + +use tokio::{ + sync::broadcast::{Receiver, Sender}, + time::sleep, +}; + +use crate::{ + config::Config, models::*, pipe::Pipe, state_machine::Event, BASE_URL, CLIENT_ID, REDIRECT_URI, +}; + +pub fn open_browser( + code_verifier: CodeVerifier, + code_challenge_method: CodeChallengeMethod, +) -> Result<(), anyhow::Error> { + let code_challenge = match code_challenge_method { + CodeChallengeMethod::Plain => { + use base64::prelude::*; + BASE64_URL_SAFE_NO_PAD.encode(code_verifier.to_string()) + } + CodeChallengeMethod::Sha256 => { + use base64::prelude::*; + BASE64_URL_SAFE_NO_PAD.encode(sha256::digest(code_verifier.to_string())) + } + }; + + let request = AuthorizeRequest::new( + CLIENT_ID, + ResponseType::Code, + None, + code_challenge.clone(), + Some(code_challenge_method.clone()), + RedirectUri::new(REDIRECT_URI), + None, + ); + + let qs = serde_qs::to_string(&request)?; + + open::that(format!("{}/oauth2/authorize?{}", BASE_URL, qs))?; + + Ok(()) +} + +pub async fn start_code_listener( + pipe_sender: Sender, + event_receiver: Receiver, +) -> Result<(), anyhow::Error> { + let pipe = Pipe::new(event_receiver.resubscribe()); + pipe.listen(pipe_sender).await?; + Ok(()) +} + +pub async fn start_code_to_token( + config: Config, + mut pipe_receiver: Receiver, + event_sender: Sender, + mut event_receiver: Receiver, +) -> Result<(), anyhow::Error> { + loop { + tokio::select! { + _ = sleep(Duration::from_millis(100)) => { + if let Ok(Event::Quit) = event_receiver.try_recv() { + println!("Shutting down Code Transformer"); + break; + } + } + Ok(response) = pipe_receiver.recv() => { + let r = AuthorizationCodeRequest::new( + GrantType::AuthorizationCode, + response.code(), + REDIRECT_URI.into(), + CLIENT_ID, + config.code_verifier().unwrap() + ); + + let qs = serde_qs::to_string(&r)?; + + let client = reqwest::Client::new(); + + let response = client + .post(format!("{}/oauth2/token", BASE_URL)) + .body(qs) + .send() + .await?; + + let response: AuthorizationCodeResponse = response.json().await?; + + event_sender.send(Event::TokenReceived { token: response.token() })?; + } + } + } + + println!("Code Transformer Shutdown"); + Ok(()) +} diff --git a/avam-client/src/pipe.rs b/avam-client/src/pipe.rs new file mode 100644 index 0000000..4205cff --- /dev/null +++ b/avam-client/src/pipe.rs @@ -0,0 +1,97 @@ +use interprocess::os::windows::named_pipe::{pipe_mode, tokio::*, PipeListenerOptions}; +use std::path::Path; +use std::time::Duration; +use thiserror::Error; +use tokio::sync::broadcast::{Receiver, Sender}; +use tokio::time::sleep; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + select, +}; + +use crate::models::AuthorizationResponse; +use crate::state_machine::Event; + +#[derive(Debug, Error)] +pub enum PipeError { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Unknown(#[from] anyhow::Error), +} + +pub struct Pipe { + quit_signal: Receiver, +} + +const PIPE_NAME: &str = "AvamICP"; + +impl Pipe { + pub fn new(quit_signal: Receiver) -> Self { + Self { quit_signal } + } + + pub async fn send(msg: &str) -> Result<(), PipeError> { + let conn = DuplexPipeStream::::connect_by_path(format!( + r"\\.\pipe\{}", + PIPE_NAME + )) + .await?; + let (_, mut sender) = conn.split(); + + sender.write_all(msg.as_bytes()).await?; + sender.shutdown().await?; + + Ok(()) + } + + pub async fn listen( + &self, + pipe_sender: Sender, + ) -> Result<(), PipeError> { + // todo: give state so we can gracefully shutdown the pipe + let mut quit_signal = self.quit_signal.resubscribe(); + let listener = PipeListenerOptions::new() + .path(Path::new(&format!(r"\\.\pipe\{}", PIPE_NAME))) + .create_tokio_duplex::()?; + + loop { + select! { + Ok(conn) = listener.accept() => { + let new_sender = pipe_sender.clone(); + tokio::spawn(async move { + if let Err(e) = Self::handle_conn(conn, new_sender).await { + eprintln!("error while handling connection: {e}"); + } + }); + }, + _ = sleep(Duration::from_millis(100)) => { + if let Ok(Event::Quit) = quit_signal.try_recv() { + println!("Shutting down Code Listener"); + break; + } + } + } + } + + println!("Code Listener Shutdown"); + Ok(()) + } + + async fn handle_conn( + conn: DuplexPipeStream, + pipe_sender: Sender, + ) -> Result<(), PipeError> { + let (mut recver, _) = conn.split(); + + let mut buffer = Vec::with_capacity(512); + + recver.read_buf(&mut buffer).await?; + + let as_string = String::from_utf8_lossy(&buffer).to_string(); + let response: AuthorizationResponse = as_string.try_into()?; + + let _ = pipe_sender.send(response); + Ok(()) + } +} diff --git a/avam-client/src/state_machine.rs b/avam-client/src/state_machine.rs new file mode 100644 index 0000000..b8120b2 --- /dev/null +++ b/avam-client/src/state_machine.rs @@ -0,0 +1,149 @@ +use tokio::sync::broadcast::{Receiver, Sender}; + +use crate::{ + config::Config, + models::{CodeChallengeMethod, CodeVerifier}, + oauth, +}; + +#[derive(Debug, PartialEq)] +pub enum State { + Init, + AppStart { + config: Config, + }, + Authenticate { + code_verifier: CodeVerifier, + code_challenge_method: CodeChallengeMethod, + }, + Connect { + token: String, + }, + WaitForSim, + Running, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + Ready { + config: Config, + }, + StartAuthenticate { + code_verifier: CodeVerifier, + code_challenge_method: CodeChallengeMethod, + }, // should not be string + TokenReceived { + token: String, + }, // AppStart and Authenticate can fire off TokenReceived to transition into Connect + Connected, // Once connected to the socket, and properly authenticated, fire off Connected to transition to WaitForSim + Disconnected, // If for whatever reason we're disconnected from the backend, we need to transition back to Connect + SimConnected, // SimConnect is connected, we're in the world and ready to send data, transition to Running + SimDisconnected, // SimConnect is disconnected, we've finished the flight and exited back to the menu, transition back to WaitForSim + Quit, +} + +impl State { + pub async fn next(self, event: Event) -> State { + match (self, event) { + // (Current State, SomeEvent) => NextState + (State::Init, Event::Ready { config }) => State::AppStart { config }, + ( + State::AppStart { .. }, + Event::StartAuthenticate { + code_verifier: code_challenge, + code_challenge_method, + }, + ) => Self::Authenticate { + code_verifier: code_challenge, + code_challenge_method, + }, // Goto Authenticate + + (State::AppStart { .. }, Event::TokenReceived { token }) => State::Connect { token }, + (State::Authenticate { .. }, Event::TokenReceived { token }) => { + State::Connect { token } + } + + (State::Connect { .. }, Event::Connected) => todo!(), // Goto WaitForSim + + (State::WaitForSim, Event::SimConnected) => todo!(), // Goto Running + + (State::Running, Event::Disconnected) => todo!(), // Goto Connect + (State::Running, Event::SimDisconnected) => todo!(), // Goto WaitForSim + + (_, Event::Quit) => todo!(), // All events can go into quit, to shutdown the application + + _ => panic!("Invalid state transition"), + } + } + + pub async fn run(&self, signal: Sender) -> Result<(), anyhow::Error> { + match self { + State::Init => Ok(()), + State::AppStart { config } => { + if let Some(token) = config.token() { + signal.send(Event::TokenReceived { + token: token.to_string(), + })?; + } else { + let code_verifier = CodeVerifier::new(); + let code_challenge_method = CodeChallengeMethod::Sha256; + + config.set_code_verifier(Some(code_verifier.clone()))?; + config.set_code_challenge_method(Some(code_challenge_method.clone()))?; + + signal.send(Event::StartAuthenticate { + code_verifier, + code_challenge_method, + })?; + } + Ok(()) + } + State::Authenticate { + code_verifier, + code_challenge_method, + } => { + oauth::open_browser(code_verifier.clone(), code_challenge_method.clone())?; + + Ok(()) + } + State::Connect { token } => { + println!("Holyshit we've got a token: {}", token); + Ok(()) + } + State::WaitForSim => Ok(()), + State::Running => Ok(()), + } + } +} + +pub async fn start( + config: Config, + event_sender: Sender, + mut event_receiver: Receiver, +) -> Result<(), anyhow::Error> { + let mut state = State::Init; + + state.run(event_sender.clone()).await?; + + loop { + if let Ok(event) = event_receiver.recv().await { + if event == Event::Quit { + println!("Shutting down State Machine"); + break; + } + + state = state.next(event).await; + + // before run + if let State::Connect { token } = &state { + // before run Connect, save the given token in config + config.set_token(Some(token.clone()))?; + } + + state.run(event_sender.clone()).await?; + } + } + + println!("State Machine Shutdown"); + Ok(()) +} diff --git a/migrations/20241012185353_oauth2.down.sql b/migrations/20241012185353_oauth2.down.sql new file mode 100644 index 0000000..074e0ef --- /dev/null +++ b/migrations/20241012185353_oauth2.down.sql @@ -0,0 +1,5 @@ +-- Add down migration script here +DROP TABLE "refresh_tokens"; +DROP TABLE "authorized_clients"; +DROP TABLE "authorization_code"; +DROP TABLE "clients"; diff --git a/migrations/20241012185353_oauth2.up.sql b/migrations/20241012185353_oauth2.up.sql new file mode 100644 index 0000000..32cd783 --- /dev/null +++ b/migrations/20241012185353_oauth2.up.sql @@ -0,0 +1,40 @@ +-- Add up migration script here + +CREATE TABLE "clients" ( + "id" uuid NOT NULL, + "user_id" uuid NOT NULL, + "name" text NOT NULL, + "secret" text NOT NULL, + "redirect_uri" text NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "clients_name_key" UNIQUE ("name"), + CONSTRAINT "clients_pkey" PRIMARY KEY ("id") +) WITH (oids = false); + +CREATE TABLE "authorization_code" ( + "code" text NOT NULL, + "user_id" uuid NOT NULL, + "client_id" uuid NOT NULL, + "code_challenge" text NOT NULL, + "code_challenge_method" text NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "authorization_code_pkey" PRIMARY KEY ("code") +) WITH (oids = false); + +CREATE TABLE "authorized_clients" ( + "user_id" uuid NOT NULL, + "client_id" uuid NOT NULL, + "created_at" timestamp DEFAULT CURRENT_TIMESTAMP, + "updated_at" timestamp DEFAULT CURRENT_TIMESTAMP, -- last accessed + CONSTRAINT "authorized_clients_pkey" PRIMARY KEY ("user_id", "client_id") +) WITH (oids = false); + +CREATE TABLE "refresh_tokens" ( + "token" text NOT NULL, + "user_id" uuid NOT NULL, + "client_id" uuid NOT NULL, + "expires_at" timestamp DEFAULT CURRENT_TIMESTAMP + interval '30' day, + CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("token") +) WITH (oids = false); diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs index 85895da..3072a2f 100644 --- a/src/bin/server/main.rs +++ b/src/bin/server/main.rs @@ -1,4 +1,5 @@ #[cfg(feature = "ssr")] +#[allow(clippy::needless_return)] #[tokio::main] async fn main() -> anyhow::Result<()> { use avam::config::Config; @@ -31,13 +32,14 @@ async fn main() -> anyhow::Result<()> { let app_state = AppState::new(api_service).await; - let http_server = HttpServer::new(app_state, postgres.pool()).await?; - http_server.run().await + HttpServer::new(app_state, postgres.pool()) + .await? + .run() + .await } #[cfg(not(feature = "ssr"))] pub fn main() { - println!("Do run this main?!"); // no client-side main function // unless we want this to work with e.g., Trunk for a purely client-side app // see lib.rs for hydration function instead diff --git a/src/lib/config.rs b/src/lib/config.rs index 501bd82..99389f2 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -8,6 +8,7 @@ const SMTP_PORT: &str = "SMTP_PORT"; const SMTP_USERNAME: &str = "SMTP_USERNAME"; const SMTP_PASSWORD: &str = "SMTP_PASSWORD"; const SMTP_SENDER: &str = "SMTP_SENDER"; +const JWT_SECRET: &str = "JWT_SECRET"; #[derive(Debug, Clone, PartialEq, Eq)] pub struct Config { @@ -18,6 +19,8 @@ pub struct Config { pub smtp_username: String, pub smtp_password: String, pub smtp_sender: String, + + pub jwt_secret: String, } impl Config { @@ -28,6 +31,7 @@ impl Config { let smtp_username = load_env(SMTP_USERNAME)?; let smtp_password = load_env(SMTP_PASSWORD)?; let smtp_sender = load_env(SMTP_SENDER)?; + let jwt_secret = load_env(JWT_SECRET)?; Ok(Config { database_url, @@ -36,6 +40,7 @@ impl Config { smtp_username, smtp_password, smtp_sender, + jwt_secret, }) } } diff --git a/src/lib/domain/api/mod.rs b/src/lib/domain/api/mod.rs index 5d9cd8c..8182d85 100644 --- a/src/lib/domain/api/mod.rs +++ b/src/lib/domain/api/mod.rs @@ -18,6 +18,7 @@ pub mod prelude { // But so far, this is the only thing that actually works pub type AppService = std::sync::Arc>; + pub use super::models::oauth::*; pub use super::models::user::*; pub use super::ports::*; pub use super::service::*; @@ -32,6 +33,7 @@ pub mod prelude { #[cfg(not(feature = "ssr"))] pub mod prelude { + pub use super::models::oauth::*; pub use super::models::user::*; pub use crate::domain::leptos::flashbag::Alert; pub use crate::domain::leptos::flashbag::Flash; diff --git a/src/lib/domain/api/models/mod.rs b/src/lib/domain/api/models/mod.rs index 22d12a3..0246ccf 100644 --- a/src/lib/domain/api/models/mod.rs +++ b/src/lib/domain/api/models/mod.rs @@ -1 +1,3 @@ +pub mod oauth; +pub mod pilot; pub mod user; diff --git a/src/lib/domain/api/models/oauth.rs b/src/lib/domain/api/models/oauth.rs new file mode 100644 index 0000000..fae507e --- /dev/null +++ b/src/lib/domain/api/models/oauth.rs @@ -0,0 +1,377 @@ +use std::{fmt::Display, str::FromStr}; + +use derive_more::derive::{Display, From}; +#[cfg(feature = "ssr")] +use rand::{distributions::Alphanumeric, Rng}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use super::user::User; + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct Client { + id: uuid::Uuid, + user_id: uuid::Uuid, + name: ClientName, + secret: ClientSecret, + redirect_uri: RedirectUri, +} + +impl Client { + pub fn new( + id: uuid::Uuid, + user_id: uuid::Uuid, + name: ClientName, + secret: ClientSecret, + redirect_uri: RedirectUri, + ) -> Self { + Self { + id, + user_id, + name, + secret, + redirect_uri, + } + } + + pub fn id(&self) -> uuid::Uuid { + self.id + } + + pub fn name(&self) -> &ClientName { + &self.name + } + + pub fn redirect_uri(&self) -> &RedirectUri { + &self.redirect_uri + } +} + +#[derive(Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct ClientName(String); + +impl ClientName { + pub fn new(name: &str) -> Self { + Self(name.to_string()) + } +} + +#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(from = "String")] +pub struct ClientSecret(String); + +impl From for ClientSecret { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for ClientSecret { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +#[cfg(feature = "ssr")] +impl ClientSecret { + pub fn new() -> Self { + let token: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(); + + Self(token) + } +} + +#[cfg(feature = "ssr")] +impl Default for ClientSecret { + fn default() -> Self { + Self::new() + } +} + +#[derive( + Clone, Debug, Display, From, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +pub struct RedirectUri(String); + +impl RedirectUri { + pub fn new(uri: &str) -> Self { + Self(uri.to_string()) + } +} + +#[cfg(feature = "ssr")] +#[derive(Debug, Error)] +pub enum CreateAuthorizationCodeError { + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(from = "String")] +pub struct AuthorizationCode(String); + +impl From for AuthorizationCode { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for AuthorizationCode { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +#[cfg(feature = "ssr")] +impl AuthorizationCode { + pub fn new() -> Self { + let token: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(64) + .map(char::from) + .collect(); + + Self(token) + } +} + +#[cfg(feature = "ssr")] +impl Default for AuthorizationCode { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct AuthorizedClient { + client: Client, + user: User, +} + +#[derive(Debug, Error)] +pub enum CodeChallengeMethodError { + #[error("Code challenge method is not valid.")] + Invalid, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum CodeChallengeMethod { + #[default] + #[serde(rename = "plain")] + Plain, + #[serde(rename = "S256")] + Sha256, +} + +impl Display for CodeChallengeMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + CodeChallengeMethod::Plain => "plain", + CodeChallengeMethod::Sha256 => "S256", + } + ) + } +} + +impl FromStr for CodeChallengeMethod { + type Err = crate::domain::api::models::oauth::CodeChallengeMethodError; + + fn from_str(s: &str) -> Result { + match s { + "plain" => Ok(Self::Plain), + "S256" => Ok(Self::Sha256), + _ => Err(crate::domain::api::models::oauth::CodeChallengeMethodError::Invalid), + } + } +} + +impl TryFrom for CodeChallengeMethod { + type Error = crate::domain::api::models::oauth::CodeChallengeMethodError; + + fn try_from(value: String) -> Result { + Self::from_str(&value) + } +} + +#[derive(Debug, Error)] +pub enum ResponseTypeError { + #[error("The response type is not valid.")] + Invalid, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum ResponseType { + #[serde(rename = "code")] + Code, +} + +impl Display for ResponseType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + ResponseType::Code => "code", + } + ) + } +} + +impl FromStr for ResponseType { + type Err = crate::domain::api::models::oauth::ResponseTypeError; + + fn from_str(s: &str) -> Result { + match s { + "code" => Ok(Self::Code), + _ => Err(crate::domain::api::models::oauth::ResponseTypeError::Invalid), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthorizeRequest { + client_id: uuid::Uuid, + response_type: ResponseType, // Make type (enum:code,) + state: Option, // random string for CSRF protection + code_challenge: String, // pkce + code_challenge_method: Option, // Make type (enum:sha256,) hashing algo + redirect_uri: RedirectUri, // Make type + scope: Option, // space seperated string with permissions +} + +impl AuthorizeRequest { + pub fn new( + client_id: uuid::Uuid, + response_type: ResponseType, + state: Option, + code_challenge: String, + code_challenge_method: Option, + redirect_uri: RedirectUri, + scope: Option, + ) -> Self { + Self { + client_id, + response_type, + state, + code_challenge, + code_challenge_method, + redirect_uri, + scope, + } + } + + pub fn client_id(&self) -> uuid::Uuid { + self.client_id + } + + pub fn response_type(&self) -> ResponseType { + self.response_type.clone() + } + + pub fn state(&self) -> Option { + self.state.clone() + } + + pub fn code_challenge(&self) -> String { + self.code_challenge.clone() + } + + pub fn code_challenge_method(&self) -> Option { + self.code_challenge_method.clone() + } + + pub fn redirect_uri(&self) -> RedirectUri { + self.redirect_uri.clone() + } + + pub fn scope(&self) -> Option { + self.scope.clone() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthorizationResponse { + code: AuthorizationCode, + state: Option, +} + +impl AuthorizationResponse { + pub fn new(code: AuthorizationCode, state: Option) -> Self { + Self { code, state } + } + + pub fn code(&self) -> AuthorizationCode { + self.code.clone() + } + + pub fn state(&self) -> Option { + self.state.clone() + } +} + +#[derive(Debug, Error)] +pub enum TokenError { + #[error("Invalid Token Request")] + InvalidRequest, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TokenSubject { + #[serde(rename = "a")] + user_id: uuid::Uuid, + #[serde(rename = "b")] + client_id: uuid::Uuid, + #[serde(skip)] + code_challenge: String, + #[serde(skip)] + code_challenge_method: CodeChallengeMethod, +} + +impl TokenSubject { + pub fn new( + user_id: uuid::Uuid, + client_id: uuid::Uuid, + code_challenge: String, + code_challenge_method: CodeChallengeMethod, + ) -> Self { + Self { + user_id, + client_id, + code_challenge, + code_challenge_method, + } + } + + pub fn user_id(&self) -> uuid::Uuid { + self.user_id + } + + pub fn client_id(&self) -> uuid::Uuid { + self.client_id + } + + pub fn code_challenge(&self) -> String { + self.code_challenge.clone() + } + + pub fn code_challenge_method(&self) -> CodeChallengeMethod { + self.code_challenge_method.clone() + } +} diff --git a/src/lib/domain/api/models/pilot.rs b/src/lib/domain/api/models/pilot.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/domain/api/models/user.rs b/src/lib/domain/api/models/user.rs index 5c4546d..03fa2cb 100644 --- a/src/lib/domain/api/models/user.rs +++ b/src/lib/domain/api/models/user.rs @@ -40,8 +40,8 @@ impl User { } } - pub fn id(&self) -> &uuid::Uuid { - &self.id + pub fn id(&self) -> uuid::Uuid { + self.id } pub fn email(&self) -> &EmailAddress { @@ -103,6 +103,13 @@ impl ActivationToken { } } +#[cfg(feature = "ssr")] +impl Default for ActivationToken { + fn default() -> Self { + Self::new() + } +} + #[cfg(feature = "ssr")] #[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize)] #[serde(from = "String")] @@ -135,6 +142,13 @@ impl PasswordResetToken { } } +#[cfg(feature = "ssr")] +impl Default for PasswordResetToken { + fn default() -> Self { + Self::new() + } +} + #[cfg(feature = "ssr")] #[derive(Debug, Error)] pub enum ResetPasswordError { @@ -410,6 +424,13 @@ impl UpdateUserRequest { } } +#[cfg(feature = "ssr")] +impl Default for UpdateUserRequest { + fn default() -> Self { + Self::new() + } +} + #[cfg(feature = "ssr")] #[derive(Debug, Error)] pub enum UpdateUserError { diff --git a/src/lib/domain/api/ports.rs b/src/lib/domain/api/ports.rs index 13b9ae3..72fa860 100644 --- a/src/lib/domain/api/ports.rs +++ b/src/lib/domain/api/ports.rs @@ -1,133 +1,9 @@ -/* - Module `ports` specifies the API by which external modules interact with the user domain. +mod api_service; +mod oauth_repository; +mod user_notifier; +mod user_repository; - All traits are bounded by `Send + Sync + 'static`, since their implementations must be shareable - between request-handling threads. - - Trait methods are explicitly asynchronous, including `Send` bounds on response types, - since the application is expected to always run in a multithreaded environment. -*/ - -use std::future::Future; - -use super::models::user::*; - -/// `ApiService` is the public API for the user domain. -/// -/// External modules must conform to this contract – the domain is not concerned with the -/// implementation details or underlying technology of any external code. -pub trait ApiService: Clone + Send + Sync + 'static { - /// Asynchronously create a new [User]. - /// - /// # Errors - /// - /// - [CreateUserError::Duplicate] if an [User] with the same [EmailAddress] already exists. - fn create_user( - &self, - req: CreateUserRequest, - ) -> impl Future> + Send; - - // fn activate_user - - fn get_user_session( - &self, - session: &axum_session::SessionAnySession, // TODO: Get rid of this and make cleaner - ) -> impl Future> + Send; - - fn activate_user_account( - &self, - token: ActivationToken, - ) -> impl Future> + Send; - - fn user_login( - &self, - req: UserLoginRequest, - ) -> impl Future> + Send; - - fn forgot_password(&self, email: &EmailAddress) -> impl Future + Send; - - fn reset_password( - &self, - token: &PasswordResetToken, - password: &Password, - ) -> impl Future> + Send; - - fn find_user_by_password_reset_token( - &self, - token: &PasswordResetToken, - ) -> impl Future> + Send; - - // These shouldnt be here, _why_ are they here, and implement that here instead - // fn find_user_by_email(self, email: EmailAddress) -> impl Future> + Send; - // fn find_user_by_id(&self, user_id: uuid::Uuid) -> impl Future> + Send; -} - -pub trait UserRepository: Clone + Send + Sync + 'static { - // Create - fn create_user( - &self, - req: CreateUserRequest, - ) -> impl Future> + Send; - - fn create_activation_token( - &self, - ent: &User, - ) -> impl Future> + Send; - - fn create_password_reset_token( - &self, - ent: &User, - ) -> impl Future> + Send; - - // Read - fn all_users(&self) -> impl Future> + Send; - - fn find_user_by_id( - &self, - id: uuid::Uuid, - ) -> impl Future, anyhow::Error>> + Send; - - fn find_user_by_email( - &self, - email: &EmailAddress, - ) -> impl Future, anyhow::Error>> + Send; - - fn find_user_by_activation_token( - &self, - token: &ActivationToken, - ) -> impl Future, anyhow::Error>> + Send; - - fn find_user_by_password_reset_token( - &self, - token: &PasswordResetToken, - ) -> impl Future, anyhow::Error>> + Send; - - // // Update - fn update_user( - &self, - ent: &User, - req: UpdateUserRequest, - ) -> impl Future> + Send; - - // Delete - // fn delete_user(&self, ent: User) -> impl Future> + Send; - fn delete_activation_token_for_user( - &self, - ent: &User, - ) -> impl Future> + Send; - - fn delete_password_reset_tokens_for_user( - &self, - ent: &User, - ) -> impl Future> + Send; -} - -pub trait UserNotifier: Clone + Send + Sync + 'static { - fn user_created(&self, user: &User, token: &ActivationToken) - -> impl Future + Send; - fn forgot_password( - &self, - user: &User, - token: &PasswordResetToken, - ) -> impl Future + Send; -} +pub use api_service::ApiService; +pub use oauth_repository::OAuthRepository; +pub use user_notifier::UserNotifier; +pub use user_repository::UserRepository; diff --git a/src/lib/domain/api/ports/api_service.rs b/src/lib/domain/api/ports/api_service.rs new file mode 100644 index 0000000..66cdf1f --- /dev/null +++ b/src/lib/domain/api/ports/api_service.rs @@ -0,0 +1,60 @@ +use crate::{ + domain::api::models::oauth::*, inbound::http::handlers::oauth::AuthorizationCodeRequest, +}; + +use super::super::models::user::*; +use std::future::Future; + +pub trait ApiService: Clone + Send + Sync + 'static { + // --- + // USER + // --- + fn create_user( + &self, + req: CreateUserRequest, + ) -> impl Future> + Send; + + fn get_user_session( + &self, + session: &axum_session::SessionAnySession, // TODO: Get rid of this and make cleaner + ) -> impl Future> + Send; + + fn activate_user_account( + &self, + token: ActivationToken, + ) -> impl Future> + Send; + + fn user_login( + &self, + req: UserLoginRequest, + ) -> impl Future> + Send; + + fn forgot_password(&self, email: &EmailAddress) -> impl Future + Send; + + fn reset_password( + &self, + token: &PasswordResetToken, + password: &Password, + ) -> impl Future> + Send; + + fn find_user_by_password_reset_token( + &self, + token: &PasswordResetToken, + ) -> impl Future> + Send; + + // --- + // OAUTH + // --- + fn find_client_by_id(&self, id: uuid::Uuid) -> impl Future> + Send; + + fn generate_authorization_code( + &self, + user: &User, + req: AuthorizeRequest, + ) -> impl Future> + Send; + + fn create_token( + &self, + req: AuthorizationCodeRequest, + ) -> impl Future, TokenError>> + Send; +} diff --git a/src/lib/domain/api/ports/oauth_repository.rs b/src/lib/domain/api/ports/oauth_repository.rs new file mode 100644 index 0000000..f966550 --- /dev/null +++ b/src/lib/domain/api/ports/oauth_repository.rs @@ -0,0 +1,33 @@ +use super::super::models::oauth::*; +use std::future::Future; + +pub trait OAuthRepository: Clone + Send + Sync + 'static { + fn find_client_by_id( + &self, + id: uuid::Uuid, + ) -> impl Future, anyhow::Error>> + Send; + + fn create_authorization_code( + &self, + user_id: uuid::Uuid, + client_id: uuid::Uuid, + code_challenge: String, + code_challenge_method: CodeChallengeMethod, + ) -> impl Future> + Send; + + fn is_authorized_client( + &self, + user_id: uuid::Uuid, + client_id: uuid::Uuid, + ) -> impl Future> + Send; + + fn get_token_subject( + &self, + code: AuthorizationCode, + ) -> impl Future, anyhow::Error>> + Send; + + fn delete_token( + &self, + code: AuthorizationCode, + ) -> impl Future> + Send; +} diff --git a/src/lib/domain/api/ports/user_notifier.rs b/src/lib/domain/api/ports/user_notifier.rs new file mode 100644 index 0000000..e856d02 --- /dev/null +++ b/src/lib/domain/api/ports/user_notifier.rs @@ -0,0 +1,12 @@ +use super::super::models::user::*; +use std::future::Future; + +pub trait UserNotifier: Clone + Send + Sync + 'static { + fn user_created(&self, user: &User, token: &ActivationToken) + -> impl Future + Send; + fn forgot_password( + &self, + user: &User, + token: &PasswordResetToken, + ) -> impl Future + Send; +} diff --git a/src/lib/domain/api/ports/user_repository.rs b/src/lib/domain/api/ports/user_repository.rs new file mode 100644 index 0000000..e44a279 --- /dev/null +++ b/src/lib/domain/api/ports/user_repository.rs @@ -0,0 +1,62 @@ +use super::super::models::user::*; +use std::future::Future; + +pub trait UserRepository: Clone + Send + Sync + 'static { + // Create + fn create_user( + &self, + req: CreateUserRequest, + ) -> impl Future> + Send; + + fn create_activation_token( + &self, + ent: &User, + ) -> impl Future> + Send; + + fn create_password_reset_token( + &self, + ent: &User, + ) -> impl Future> + Send; + + // Read + fn all_users(&self) -> impl Future> + Send; + + fn find_user_by_id( + &self, + id: uuid::Uuid, + ) -> impl Future, anyhow::Error>> + Send; + + fn find_user_by_email( + &self, + email: &EmailAddress, + ) -> impl Future, anyhow::Error>> + Send; + + fn find_user_by_activation_token( + &self, + token: &ActivationToken, + ) -> impl Future, anyhow::Error>> + Send; + + fn find_user_by_password_reset_token( + &self, + token: &PasswordResetToken, + ) -> impl Future, anyhow::Error>> + Send; + + // // Update + fn update_user( + &self, + ent: &User, + req: UpdateUserRequest, + ) -> impl Future> + Send; + + // Delete + // fn delete_user(&self, ent: User) -> impl Future> + Send; + fn delete_activation_token_for_user( + &self, + ent: &User, + ) -> impl Future> + Send; + + fn delete_password_reset_tokens_for_user( + &self, + ent: &User, + ) -> impl Future> + Send; +} diff --git a/src/lib/domain/api/service.rs b/src/lib/domain/api/service.rs index 10128f2..77dadbb 100644 --- a/src/lib/domain/api/service.rs +++ b/src/lib/domain/api/service.rs @@ -4,17 +4,22 @@ */ use axum_session::SessionAnySession; +use crate::inbound::http::handlers::oauth::AuthorizationCodeRequest; +use crate::inbound::http::handlers::oauth::GrantType; + +use super::models::oauth::Client; +use super::models::oauth::*; use super::models::user::*; -use super::ports::{ApiService, UserNotifier, UserRepository}; +use super::ports::{ApiService, OAuthRepository, UserNotifier, UserRepository}; -pub trait Repository = UserRepository; +// pub trait Repository = UserRepository + OAuthRepository; pub trait Email = UserNotifier; #[derive(Debug, Clone)] pub struct Service where - R: Repository, + R: UserRepository + OAuthRepository, N: Email, { repo: R, @@ -23,7 +28,7 @@ where impl Service where - R: Repository, + R: UserRepository + OAuthRepository, N: Email, { pub fn new(repo: R, notifier: N) -> Self { @@ -33,12 +38,13 @@ where impl ApiService for Service where - R: UserRepository, + R: UserRepository + OAuthRepository, N: Email, { async fn create_user(&self, req: CreateUserRequest) -> Result { let result = self.repo.create_user(req).await; + #[allow(clippy::question_mark)] if result.is_err() { // something went wrong, log the error // but keep passing on the result to the requester (http server) @@ -56,9 +62,7 @@ where } async fn get_user_session(&self, session: &SessionAnySession) -> Option { - let Some(user_id) = session.get("user") else { - return None; - }; + let user_id = session.get("user")?; self.repo.find_user_by_id(user_id).await.unwrap_or(None) } @@ -69,7 +73,7 @@ where ) -> Result { let user = match self.repo.find_user_by_activation_token(&token).await { Ok(u) => u, - Err(e) => return Err(ActivateUserError::Unknown(e.into())), + Err(e) => return Err(ActivateUserError::Unknown(e)), }; let Some(user) = user else { @@ -98,7 +102,7 @@ where Ok(u) => u, Err(e) => { tracing::error!("{:#?}", e); - return Err(UserLoginError::Unknown(e.into())); + return Err(UserLoginError::Unknown(e)); } }; @@ -162,7 +166,7 @@ where Ok(u) => u, Err(e) => { tracing::error!("{:#?}", e); - return Err(ResetPasswordError::Unknown(e.into())); + return Err(ResetPasswordError::Unknown(e)); } }; @@ -190,4 +194,77 @@ where .await .unwrap_or(None) } + + async fn find_client_by_id(&self, id: uuid::Uuid) -> Option { + self.repo.find_client_by_id(id).await.ok().flatten() + } + + async fn generate_authorization_code( + &self, + user: &User, + req: AuthorizeRequest, + ) -> Result { + let Some(client) = self.repo.find_client_by_id(req.client_id()).await? else { + return Err(anyhow::anyhow!("Client not found")); + }; + + if client.redirect_uri() != &req.redirect_uri() { + return Err(anyhow::anyhow!("Invalid redirect uri")); + } + + let code = self + .repo + .create_authorization_code( + user.id(), + client.id(), + req.code_challenge(), + req.code_challenge_method().unwrap_or_default(), + ) + .await?; + + Ok(AuthorizationResponse::new(code, req.state())) + } + + async fn create_token( + &self, + req: AuthorizationCodeRequest, + ) -> Result, TokenError> { + if req.grant_type() != GrantType::AuthorizationCode { + return Err(TokenError::InvalidRequest); + } + + let code = req.code(); + + let Some(token) = self.repo.get_token_subject(code).await? else { + return Err(TokenError::InvalidRequest); + }; + + let code_verifier = req.code_verifier(); + let code_challenge = match token.code_challenge_method() { + CodeChallengeMethod::Plain => { + use base64::prelude::*; + BASE64_URL_SAFE_NO_PAD.encode(code_verifier.to_string()) + } + CodeChallengeMethod::Sha256 => { + use base64::prelude::*; + BASE64_URL_SAFE_NO_PAD.encode(sha256::digest(code_verifier.to_string())) + } + }; + + if token.code_challenge() != code_challenge { + return Err(TokenError::InvalidRequest); + } + + let Some(client) = self.repo.find_client_by_id(token.client_id()).await? else { + return Err(TokenError::InvalidRequest); // no such client + }; + + if &req.redirect_uri() != client.redirect_uri() { + return Err(TokenError::InvalidRequest); // invalid redirect uri + } + + let _ = self.repo.delete_token(req.code()).await; + + Ok(Some(token)) + } } diff --git a/src/lib/domain/leptos/app.rs b/src/lib/domain/leptos/app.rs index 30c349c..148220e 100644 --- a/src/lib/domain/leptos/app.rs +++ b/src/lib/domain/leptos/app.rs @@ -11,16 +11,23 @@ use pages::{ }, dashboard::DashboardPage, error::{AppError, ErrorTemplate}, + oauth2::authorize::AuthorizePage, }; +use crate::domain::api::prelude::User; + #[component] pub fn App() -> impl IntoView { provide_meta_context(); - let trigger_user = create_rw_signal(true); + let trigger_update = create_rw_signal(false); + let trigger_direct = create_rw_signal(None::); - let user = create_resource(trigger_user, move |_| async move { - super::check_user().await.unwrap() + let user_signal = create_rw_signal(None::); + + let user_resource = create_local_resource(trigger_update, move |_| async move { + let user = super::check_user().await.unwrap(); + user_signal.set(user); }); view! { @@ -47,17 +54,17 @@ pub fn App() -> impl IntoView { }>
- - - + + } }> - } /> + } /> @@ -66,8 +73,11 @@ pub fn App() -> impl IntoView { - - + + @@ -75,13 +85,22 @@ pub fn App() -> impl IntoView { }> - - + + } /> - // dashboard - } /> + + + + + + + } /> + + // Logged in + } />
diff --git a/src/lib/domain/leptos/app/pages/auth/login.rs b/src/lib/domain/leptos/app/pages/auth/login.rs index 9e4876a..72732fe 100644 --- a/src/lib/domain/leptos/app/pages/auth/login.rs +++ b/src/lib/domain/leptos/app/pages/auth/login.rs @@ -1,10 +1,12 @@ + use leptos::*; use leptos_router::*; -use crate::domain::leptos::app::components::alert::Alert; - +use crate::domain::api::prelude::*; use super::auth_base::AuthBase; +use crate::domain::leptos::app::components::alert::Alert as AlertView; + #[server] async fn login_action(email: String, password: String) -> Result<(), ServerFnError> { use crate::domain::api::prelude::*; @@ -26,13 +28,27 @@ async fn login_action(email: String, password: String) -> Result<(), ServerFnErr /// Renders the home page of your application. #[component] -pub fn LoginPage(user_signal: RwSignal) -> impl IntoView { +pub fn LoginPage(trigger_signal: RwSignal, direct_signal: RwSignal>) -> impl IntoView { let submit = Action::::server(); let response = submit.value().read_only(); + + let callback = move || { + let query = use_query_map(); + let Some(c) = query.with(|q| q.get("c").cloned()) else { + return Some("/".to_string()); + }; + + use base64::prelude::*; + let dec = BASE64_URL_SAFE_NO_PAD.decode(c).unwrap_or_default(); + let c = String::from_utf8_lossy(&dec); + Some(c.to_string()) + }; + create_effect(move |_| { if response.get().is_some() { - user_signal.set(!user_signal.get_untracked()); + trigger_signal.set(!trigger_signal.get_untracked()); + direct_signal.set(callback()); } }); @@ -50,25 +66,22 @@ pub fn LoginPage(user_signal: RwSignal) -> impl IntoView { - + - + { flash().unwrap().unwrap().message() } - + diff --git a/src/lib/domain/leptos/app/pages/auth/logout.rs b/src/lib/domain/leptos/app/pages/auth/logout.rs index f261ac3..0b5d0b7 100644 --- a/src/lib/domain/leptos/app/pages/auth/logout.rs +++ b/src/lib/domain/leptos/app/pages/auth/logout.rs @@ -1,5 +1,7 @@ use leptos::*; -use leptos_router::Redirect; +use leptos_router::{use_query_map, Redirect}; + +use crate::domain::api::prelude::User; #[server] async fn logout_action() -> Result<(), ServerFnError> { @@ -12,22 +14,31 @@ async fn logout_action() -> Result<(), ServerFnError> { } #[component] -pub fn LogoutPage(user_signal: RwSignal) -> impl IntoView { - let submit = Action::::server(); - let response = submit.value().read_only(); +pub fn LogoutPage( + trigger_signal: RwSignal, + user_signal: RwSignal>, +) -> impl IntoView { + let direct_signal = create_rw_signal(None::); - create_effect(move |_| { - if response.get().is_some() { - user_signal.set(!user_signal.get_untracked()); - } - }); + create_local_resource( + || (), + move |_| async move { + logout_action().await.unwrap(); + trigger_signal.set(!trigger_signal.get_untracked()); - submit.dispatch(LogoutAction {}); + let query = use_query_map(); + if let Some(c) = query.with_untracked(|q| q.get("c").cloned()) { + direct_signal.set(Some(c)); + } else { + direct_signal.set(Some("Lw".to_string())); + } + }, + ); view! { - - + + } diff --git a/src/lib/domain/leptos/app/pages/auth/reset.rs b/src/lib/domain/leptos/app/pages/auth/reset.rs index c997d1e..e76aac1 100644 --- a/src/lib/domain/leptos/app/pages/auth/reset.rs +++ b/src/lib/domain/leptos/app/pages/auth/reset.rs @@ -19,9 +19,7 @@ async fn reset_action(token: String, password: String, confirm_password: String) app.reset_password(&token.into(), &password).await.map_err(|e| format!("{}", e))?; let flash = FlashMessage::new("login", - format!( - "Your password has been reset." - )).with_alert(Alert::Success); + "Your password has been reset.".to_string()).with_alert(Alert::Success); flashbag.set(flash); diff --git a/src/lib/domain/leptos/app/pages/mod.rs b/src/lib/domain/leptos/app/pages/mod.rs index d7c0147..22a5f82 100644 --- a/src/lib/domain/leptos/app/pages/mod.rs +++ b/src/lib/domain/leptos/app/pages/mod.rs @@ -1,3 +1,4 @@ pub mod auth; pub mod dashboard; pub mod error; +pub mod oauth2; diff --git a/src/lib/domain/leptos/app/pages/oauth2/authorize.rs b/src/lib/domain/leptos/app/pages/oauth2/authorize.rs new file mode 100644 index 0000000..0a92db0 --- /dev/null +++ b/src/lib/domain/leptos/app/pages/oauth2/authorize.rs @@ -0,0 +1,279 @@ +use std::str::FromStr; +use std::sync::Arc; + +use leptos::*; +use leptos_router::*; +use serde::{Deserialize, Serialize}; + +use crate::domain::api::prelude::*; +use crate::domain::leptos::app::components::alert::Alert as AlertView; + +use crate::domain::leptos::app::pages::auth::auth_base::AuthBase; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct AuthorizationResponse { + code: String, + state: String, +} + +#[server] +async fn authorize_action(form: AuthorizeQuery) -> Result<(), ServerFnError> { + use crate::domain::api::prelude::*; + use crate::domain::leptos::check_user; + + let app = use_context::().unwrap(); + + let Some(client) = app.find_client_by_id(form.client_id()).await else { + return Err(ServerFnError::WrappedServerError( + "Invalid Client ID".to_string(), + )); + }; + + let Some(user) = check_user().await? else { + return Err(ServerFnError::WrappedServerError("No user".to_string())); + }; + + let response: AuthorizationResponse = app + .generate_authorization_code(&user, form.into()) + .await + .map_err(|e| format!("{}", e))?; + + let qs = serde_qs::to_string(&response).map_err(|e| format!("{}", e))?; + + leptos_axum::redirect(&format!("{}?{}", client.redirect_uri(), qs)); + + Ok(()) +} + +impl IntoAttribute for CodeChallengeMethod { + fn into_attribute(self) -> Attribute { + Attribute::String(self.to_string().into()) + } + + fn into_attribute_boxed(self: Box) -> Attribute { + Attribute::String(self.to_string().into()) + } +} + +// Make the types and validators for specific shit like response type and code challenge method +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct AuthorizeQuery { + client_id: uuid::Uuid, + response_type: ResponseType, // Make type (enum:code,) + state: Option, // random string for CSRF protection + code_challenge: String, // pkce + code_challenge_method: Option, // Make type (enum:sha256,) hashing algo + redirect_uri: RedirectUri, // Make type + scope: Option, // space seperated string with permissions +} + +impl From for AuthorizeRequest { + fn from(value: AuthorizeQuery) -> Self { + Self::new( + value.client_id(), + value.response_type(), + value.state(), + value.code_challenge(), + value.code_challenge_method(), + value.redirect_uri(), + value.scope(), + ) + } +} + +impl AuthorizeQuery { + pub fn client_id(&self) -> uuid::Uuid { + self.client_id + } + + pub fn response_type(&self) -> ResponseType { + self.response_type.clone() + } + + pub fn state(&self) -> Option { + self.state.clone() + } + + pub fn code_challenge(&self) -> String { + self.code_challenge.clone() + } + + pub fn code_challenge_method(&self) -> Option { + self.code_challenge_method.clone() + } + + pub fn redirect_uri(&self) -> RedirectUri { + self.redirect_uri.clone() + } + + pub fn scope(&self) -> Option { + self.scope.clone() + } +} + +impl Params for AuthorizeQuery { + fn from_map(map: &ParamsMap) -> Result { + let client_id: uuid::Uuid = match map.get("client_id") { + Some(c) => uuid::Uuid::from_str(c).map_err(|e| ParamsError::Params(Arc::new(e)))?, + None => return Err(ParamsError::MissingParam("client_id".to_string())), + }; + + let response_type: ResponseType = match map.get("response_type") { + Some(c) => ResponseType::from_str(c).map_err(|e| ParamsError::Params(Arc::new(e)))?, + None => return Err(ParamsError::MissingParam("response_type".to_string())), + }; + + let state = map.get("state").cloned(); + + let Some(code_challenge) = map.get("code_challenge").cloned() else { + return Err(ParamsError::MissingParam("code_challenge".to_string())); + }; + + let code_challenge_method = map + .get("code_challenge_method") + .map(|c| CodeChallengeMethod::from_str(c).map_err(|e| ParamsError::Params(Arc::new(e)))) + .transpose()?; + + let redirect_uri: RedirectUri = match map.get("redirect_uri") { + Some(c) => RedirectUri::new(c), + None => return Err(ParamsError::MissingParam("redirect_uri".to_string())), + }; + + let scope = map.get("scope").cloned(); + + Ok(Self { + client_id, + response_type, + state, + code_challenge, + code_challenge_method, + redirect_uri, + scope, + }) + } +} + +#[server] +async fn get_client(client_id: uuid::Uuid) -> Result> { + use crate::domain::api::prelude::*; + + let app = use_context::().unwrap(); + + let Some(client) = app.find_client_by_id(client_id).await else { + return Err(ServerFnError::WrappedServerError( + "Invalid Client ID".to_string(), + )); + }; + + Ok(client) +} + +/// Renders the home page of your application. +#[component] +pub fn AuthorizePage(user: RwSignal>) -> impl IntoView { + let submit = Action::::server(); + let response = submit.value().read_only(); + + let query = use_query::(); + + let client_signal = create_rw_signal(None::); + + let client_resource = create_local_resource( + || (), + move |_| async move { + let query = query.get_untracked().map_err(|e| format!("{}", e))?; + let client = get_client(query.client_id()) + .await + .map_err(|e| format!("{}", e))?; + + if client.redirect_uri() != &query.redirect_uri() { + return Err("Invalid redirect uri".to_string()); + } + + client_signal.set(Some(client)); + + Ok::<(), String>(()) + }, + ); + + view! { + + + + + + { move || if let Some(Err(e)) = response.get() { + {format!("{}", e)}.into_view() + } else { + ().into_view() + }} + + + + + + + { move || if let Err(e) = query.get().map_err(|e| { leptos::logging::warn!("{:#?}", e); "Invalid parameters".to_string() }) { + {e.to_string()}.into_view() + } else { + ().into_view() + }} + + + + + + + { move || if let Some(Err(e)) = client_resource.get() { + {e.to_string()}.into_view() + } else { + ().into_view() + }} + + + + + + +
+
+ "Signed in as "{move || user.get().unwrap().email().to_string()} +
+ + + + "You can now close this window." + + + + +
+ {move || client_signal().unwrap().name().to_string() }" is requesting access to your account" +
+ +
+ + + + + + + + +
+ +
+ "Not "{move || user.get().unwrap().email().to_string()}"? ""Logout""!" +
+
+
+
+
+ +
+ + } +} diff --git a/src/lib/domain/leptos/app/pages/oauth2/mod.rs b/src/lib/domain/leptos/app/pages/oauth2/mod.rs new file mode 100644 index 0000000..f12b002 --- /dev/null +++ b/src/lib/domain/leptos/app/pages/oauth2/mod.rs @@ -0,0 +1 @@ +pub mod authorize; diff --git a/src/lib/domain/leptos/flashbag.rs b/src/lib/domain/leptos/flashbag.rs index 0d074e8..f32b20f 100644 --- a/src/lib/domain/leptos/flashbag.rs +++ b/src/lib/domain/leptos/flashbag.rs @@ -46,7 +46,7 @@ where pub fn new(name: &str, message: S) -> Self { Self { name: name.to_string(), - message: message.into(), + message, alert: Alert::None, } } @@ -82,14 +82,12 @@ impl FlashBag { pub fn get(&self, flash_name: &str) -> Option { let name = format!("__flash:{}__", flash_name); - let Some(message) = self.session.get(&name) else { - return None; - }; + let message = self.session.get(&name)?; let alert_name = format!("__flash_alert:{}__", flash_name); let alert = self.session.get(&alert_name).unwrap_or(Alert::None); - self.clear(&flash_name); + self.clear(flash_name); Some(Flash { name, diff --git a/src/lib/inbound/http.rs b/src/lib/inbound/http.rs index bff9943..7b172e3 100644 --- a/src/lib/inbound/http.rs +++ b/src/lib/inbound/http.rs @@ -12,9 +12,7 @@ use axum::{ use axum_session::{SessionAnyPool, SessionConfig, SessionLayer, SessionStore}; use axum_session_sqlx::SessionPgPool; use handlers::{ - fileserv::file_and_error_handler, - leptos::{leptos_routes_handler, server_fn_handler}, - user::activate_account, + fileserv::file_and_error_handler, leptos::{leptos_routes_handler, server_fn_handler}, oauth, user::activate_account }; use leptos_axum::{generate_route_list, LeptosRoutes}; use state::AppState; @@ -59,6 +57,7 @@ impl HttpServer { ); let router = axum::Router::new() + .nest("/oauth2", oauth::routes()) .route("/auth/activate/:token", get(activate_account)) .route("/api/*fn_name", post(server_fn_handler)) .leptos_routes_with_handler(generate_route_list(App), get(leptos_routes_handler)) diff --git a/src/lib/inbound/http/handlers/mod.rs b/src/lib/inbound/http/handlers/mod.rs index 02f7ea5..b2a2711 100644 --- a/src/lib/inbound/http/handlers/mod.rs +++ b/src/lib/inbound/http/handlers/mod.rs @@ -1,3 +1,4 @@ pub mod fileserv; pub mod leptos; +pub mod oauth; pub mod user; diff --git a/src/lib/inbound/http/handlers/oauth.rs b/src/lib/inbound/http/handlers/oauth.rs new file mode 100644 index 0000000..60d3cc4 --- /dev/null +++ b/src/lib/inbound/http/handlers/oauth.rs @@ -0,0 +1,192 @@ +use std::{fmt::Display, str::FromStr}; + +use axum::{extract::State, routing::post, Json, Router}; +use derive_more::derive::Display; +use http::StatusCode; +use jsonwebtoken::{encode, EncodingKey, Header}; +use rand::{distributions::Alphanumeric, Rng}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::{config::Config, domain::api::prelude::*, inbound::http::state::AppState}; + +pub fn routes() -> axum::Router> +where + S: ApiService, +{ + Router::new().route("/token", post(token)) +} + +#[derive(Debug, Error)] +pub enum GrantTypeError { + #[error("The grant type is not valid.")] + Invalid, + #[error(transparent)] + Unknown(#[from] anyhow::Error), + // to be extended as new error scenarios are introduced +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum GrantType { + #[serde(rename = "authorization_code")] + AuthorizationCode, +} + +impl Display for GrantType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + GrantType::AuthorizationCode => "authorization_code", + } + ) + } +} + +impl FromStr for GrantType { + type Err = GrantTypeError; + + fn from_str(s: &str) -> Result { + match s { + "authorization_code" => Ok(Self::AuthorizationCode), + _ => Err(GrantTypeError::Invalid), + } + } +} + +#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(from = "String")] +pub struct CodeVerifier(String); + +impl From for CodeVerifier { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for CodeVerifier { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +impl CodeVerifier { + pub fn new() -> Self { + let token: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(120) + .map(char::from) + .collect(); + + Self(token) + } +} + +impl Default for CodeVerifier { + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthorizationCodeRequest { + grant_type: GrantType, + code: AuthorizationCode, + redirect_uri: RedirectUri, + client_id: uuid::Uuid, + code_verifier: CodeVerifier, +} + +impl AuthorizationCodeRequest { + pub fn grant_type(&self) -> GrantType { + self.grant_type.clone() + } + + pub fn code(&self) -> AuthorizationCode { + self.code.clone() + } + + pub fn redirect_uri(&self) -> RedirectUri { + self.redirect_uri.clone() + } + + pub fn client_id(&self) -> uuid::Uuid { + self.client_id + } + + pub fn code_verifier(&self) -> CodeVerifier { + self.code_verifier.clone() + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenClaims +where + T: Serialize, +{ + pub sub: T, + pub iat: usize, + pub exp: usize, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthorizationCodeResponse { + token: String, +} + +async fn token( + State(app_state): State>, + body: String, +) -> Result, (StatusCode, String)> { + let request: AuthorizationCodeRequest = serde_qs::from_str(&body).map_err(|e| { + tracing::error!("{:?}", e); + + ( + StatusCode::INTERNAL_SERVER_ERROR, + String::from("Internal Server Error"), + ) + })?; + + let app = app_state.api_service(); + let token = app.create_token(request).await.map_err(|e| { + tracing::error!("{:?}", e); + + ( + StatusCode::INTERNAL_SERVER_ERROR, + String::from("Internal Server Error"), + ) + })?; + + let token = create_token(token).map_err(|e| { + tracing::error!("{:?}", e); + + ( + StatusCode::INTERNAL_SERVER_ERROR, + String::from("Internal Server Error"), + ) + })?; + + Ok(Json(AuthorizationCodeResponse { token })) +} + +fn create_token(sub: T) -> Result +where + T: Serialize, +{ + let config = Config::from_env()?; + + let now = time::OffsetDateTime::now_utc(); + let iat = now.unix_timestamp() as usize; + let exp = (now + time::Duration::days(30)).unix_timestamp() as usize; + + let claims = TokenClaims { sub, iat, exp }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(config.jwt_secret.as_bytes()), + )?; + + Ok(token) +} diff --git a/src/lib/lib.rs b/src/lib/lib.rs index 2e94558..742cc87 100644 --- a/src/lib/lib.rs +++ b/src/lib/lib.rs @@ -1,7 +1,9 @@ #![feature(trait_alias)] +pub static BASE_URL: &str = "https://avam.avii.nl"; pub static PROJECT_NAME: &str = "Avii's Virtual Airline Manager"; pub static COPYRIGHT: &str = "Avii's Virtual Airline Manager © 2024"; + pub mod domain; #[cfg(feature = "ssr")] diff --git a/src/lib/outbound/dangerous_lettre/user_notifier.rs b/src/lib/outbound/dangerous_lettre/user_notifier.rs index 1094f23..ffb86a9 100644 --- a/src/lib/outbound/dangerous_lettre/user_notifier.rs +++ b/src/lib/outbound/dangerous_lettre/user_notifier.rs @@ -3,6 +3,7 @@ use lettre::AsyncTransport; use crate::domain::api::ports::UserNotifier; use crate::domain::api::models::user::*; +use crate::BASE_URL; use super::DangerousLettre; @@ -10,7 +11,7 @@ impl UserNotifier for DangerousLettre { async fn user_created(&self, user: &User, token: &ActivationToken) { let mut context = tera::Context::new(); - let url = format!("http://127.0.0.1:3000/auth/activate/{}", token); // Move base url to env + let url = format!("{}/auth/activate/{}", BASE_URL, token); // Move base url to env context.insert("activate_url", &url); @@ -28,7 +29,7 @@ impl UserNotifier for DangerousLettre { async fn forgot_password(&self, user: &User, token: &PasswordResetToken) { let mut context = tera::Context::new(); - let url = format!("http://127.0.0.1:3000/auth/reset/{}", token); // Move base url to env + let url = format!("{}/auth/reset/{}", BASE_URL, token); // Move base url to env context.insert("reset_url", &url); diff --git a/src/lib/outbound/postgres.rs b/src/lib/outbound/postgres.rs index fc5b690..3cebb53 100644 --- a/src/lib/outbound/postgres.rs +++ b/src/lib/outbound/postgres.rs @@ -1,3 +1,4 @@ +pub mod oauth_repository; pub mod user_repository; use std::str::FromStr; diff --git a/src/lib/outbound/postgres/oauth_repository.rs b/src/lib/outbound/postgres/oauth_repository.rs new file mode 100644 index 0000000..4ec98b3 --- /dev/null +++ b/src/lib/outbound/postgres/oauth_repository.rs @@ -0,0 +1,179 @@ +use anyhow::Context; +use sqlx::Executor; +// use sqlx::QueryBuilder; +use sqlx::Row; + +use crate::domain::api::models::oauth::*; +use crate::domain::api::ports::OAuthRepository; + +use super::Postgres; + +impl OAuthRepository for Postgres { + async fn find_client_by_id(&self, id: uuid::Uuid) -> Result, anyhow::Error> { + let query = sqlx::query( + r#" + SELECT + id, + user_id, + name, + secret, + redirect_uri + FROM clients WHERE id = $1 + "#, + ) + .bind(id); + + let row = self + .pool + .fetch_optional(query) + .await + .context("failed to execute SQL transaction")?; + + let Some(row) = row else { + return Ok(None); + }; + + let id = row.get("id"); + let user_id = row.get("user_id"); + let name = ClientName::new(row.get("name")); + let secret = ClientSecret::from(row.get::<&str, &str>("secret")); + let redirect_uri = RedirectUri::new(row.get("redirect_uri")); + + Ok(Some(Client::new(id, user_id, name, secret, redirect_uri))) + } + + async fn create_authorization_code( + &self, + user_id: uuid::Uuid, + client_id: uuid::Uuid, + code_challenge: String, + code_challenge_method: CodeChallengeMethod, + ) -> Result { + let mut tx = self + .pool + .begin() + .await + .context("Failed to start sql transaction")?; + + let code = AuthorizationCode::new(); + + let query = sqlx::query( + r#" + INSERT INTO authorization_code (code, user_id, client_id, code_challenge, code_challenge_method) + VALUES ($1, $2, $3, $4, $5) + "#, + ) + .bind(code.to_string()) + .bind(user_id) + .bind(client_id) + .bind(code_challenge) + .bind(code_challenge_method.to_string()); + + tx.execute(query) + .await + .map_err(|e| CreateAuthorizationCodeError::Unknown(e.into()))?; + + let query = sqlx::query( + r#" + INSERT INTO authorized_clients (user_id, client_id) + VALUES ($1, $2) ON CONFLICT DO NOTHING + "#, + ) + .bind(user_id) + .bind(client_id); + + tx.execute(query) + .await + .map_err(|e| CreateAuthorizationCodeError::Unknown(e.into()))?; + + tx.commit() + .await + .context("failed to commit SQL transaction")?; + + Ok(code) + } + + async fn is_authorized_client( + &self, + user_id: uuid::Uuid, + client_id: uuid::Uuid, + ) -> Result { + let query = sqlx::query( + r#" + SELECT + * + FROM authorized_clients WHERE user_id = $1 AND client_id = $2 + "#, + ) + .bind(user_id) + .bind(client_id); + + Ok(self + .pool + .fetch_optional(query) + .await + .context("failed to execute SQL transaction")? + .is_some()) + } + + async fn get_token_subject( + &self, + code: AuthorizationCode, + ) -> Result, anyhow::Error> { + let query = sqlx::query( + r#" + SELECT + user_id, + client_id, + code_challenge, + code_challenge_method + FROM authorization_code WHERE code = $1 + "#, + ) + .bind(code.to_string()); + + let Some(row) = self + .pool + .fetch_optional(query) + .await + .context("failed to execute SQL transaction")? + else { + return Ok(None); + }; + + let user_id: uuid::Uuid = row.get("user_id"); + let client_id: uuid::Uuid = row.get("client_id"); + + let code_challenge: String = row.get("code_challenge"); + let code_challenge_method: CodeChallengeMethod = row + .get::("code_challenge_method") + .try_into()?; + + Ok(Some(TokenSubject::new( + user_id, + client_id, + code_challenge, + code_challenge_method, + ))) + } + + async fn delete_token(&self, code: AuthorizationCode) -> Result<(), anyhow::Error> { + let mut tx = self + .pool + .begin() + .await + .context("Failed to start sql transaction")?; + + let query = + sqlx::query("DELETE FROM authorization_code WHERE code = $1").bind(code.to_string()); + + tx.execute(query) + .await + .context("failed to execute SQL transaction")?; + tx.commit() + .await + .context("failed to commit SQL transaction")?; + + Ok(()) + } +} diff --git a/src/lib/outbound/postgres/user_repository.rs b/src/lib/outbound/postgres/user_repository.rs index 86f9c32..b4f6ce1 100644 --- a/src/lib/outbound/postgres/user_repository.rs +++ b/src/lib/outbound/postgres/user_repository.rs @@ -216,7 +216,7 @@ impl UserRepository for Postgres { let id = row.get("user_id"); - Ok(self.find_user_by_id(id).await?) + self.find_user_by_id(id).await } async fn find_user_by_password_reset_token( @@ -244,7 +244,7 @@ impl UserRepository for Postgres { let id = row.get("user_id"); - Ok(self.find_user_by_id(id).await?) + self.find_user_by_id(id).await } async fn update_user( @@ -295,10 +295,10 @@ impl UserRepository for Postgres { Ok(( ent.clone(), User::new( - ent.id().clone(), + ent.id(), new_email.clone(), new_password.clone(), - new_verified.clone(), + *new_verified, ), )) } diff --git a/templates/user/user_notifier.rs b/templates/user/user_notifier.rs deleted file mode 100644 index 61e2eff..0000000 --- a/templates/user/user_notifier.rs +++ /dev/null @@ -1,50 +0,0 @@ -use lettre::AsyncTransport; - -use crate::domain::api::ports::UserNotifier; - -use crate::domain::api::models::user::*; - -use super::DangerousLettre; - -impl UserNotifier for DangerousLettre { - async fn user_created(&self, user: &User, token: &ActivationToken) { - let mut context = tera::Context::new(); - - let url = format!("http://127.0.0.1:3000/auth/activate/{}", token); // Move base url to env - - context.insert("activate_url", &url); - - let to = format!("{}", user.email()).parse().unwrap(); - - let message = self - .template("user/created", "Welcome to AVAM!", &context, to) - .unwrap(); - - if let Err(e) = self.mailer.send(message).await { - eprintln!("{:#?}", e); - }; - } - - async fn forgot_password(&self, user: &User, token: &ForgotPasswordToken) { - let mut context = tera::Context::new(); - - let url = format!("http://127.0.0.1:3000/auth/reset/{}", token); // Move base url to env - - context.insert("reset_url", &url); - - let to = format!("{}", user.email()).parse().unwrap(); - - let message = self - .template( - "user/password_reset", - "Password reset request", - &context, - to, - ) - .unwrap(); - - if let Err(e) = self.mailer.send(message).await { - eprintln!("{:#?}", e); - }; - } -}