From f7e1d1539fb7f4e1d9f9181513fabe12b90c9a3e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 31 Oct 2025 17:59:20 +0100 Subject: [PATCH 001/124] Reset repository content --- .drone.yml | 12 - Cargo.lock | 4256 ------ Cargo.toml | 35 - Makefile | 14 - assets/bootstrap.css | 12199 ---------------- assets/script.js | 30 - assets/style.css | 12 - assets/ws_debug.js | 68 - docker/dex/dex.config.yaml | 26 - .gitignore => matrixgw_backend/.gitignore | 0 matrixgw_backend/Cargo.toml | 6 + Dockerfile => matrixgw_backend/Dockerfile | 0 .../docker-compose.yml | 8 - .../docker}/element/config.json | 0 .../docker}/mas/config.yaml | 0 .../docker}/synapse/homeserver.yaml | 0 .../docker}/synapse/localhost.log.config | 0 .../docker}/synapse/localhost.signing.key | 0 .../examples}/api_curl.rs | 0 matrixgw_backend/src/main.rs | 3 + matrixgw_frontend/.gitignore | 24 + matrixgw_frontend/README.md | 73 + matrixgw_frontend/eslint.config.js | 23 + {assets => matrixgw_frontend}/favicon.png | Bin matrixgw_frontend/index.html | 13 + matrixgw_frontend/package-lock.json | 3265 +++++ matrixgw_frontend/package.json | 33 + matrixgw_frontend/public/vite.svg | 1 + matrixgw_frontend/src/App.css | 42 + matrixgw_frontend/src/App.tsx | 35 + matrixgw_frontend/src/assets/react.svg | 1 + matrixgw_frontend/src/index.css | 68 + matrixgw_frontend/src/main.tsx | 10 + matrixgw_frontend/tsconfig.app.json | 28 + matrixgw_frontend/tsconfig.json | 7 + matrixgw_frontend/tsconfig.node.json | 26 + matrixgw_frontend/vite.config.ts | 7 + src/app_config.rs | 187 - src/broadcast_messages.rs | 46 - src/constants.rs | 18 - src/extractors/client_auth.rs | 256 - src/extractors/mod.rs | 1 - src/lib.rs | 8 - src/main.rs | 80 - src/server/api/account.rs | 23 - src/server/api/media.rs | 59 - src/server/api/mod.rs | 14 - src/server/api/profile.rs | 29 - src/server/api/room.rs | 81 - src/server/api/ws.rs | 176 - src/server/mod.rs | 49 - src/server/web_ui.rs | 252 - src/sync_client.rs | 145 - src/user.rs | 241 - src/utils/base_utils.rs | 20 - src/utils/matrix_utils.rs | 18 - src/utils/mod.rs | 2 - templates/base_page.html | 46 - templates/index.html | 146 - templates/ws_debug.html | 46 - 60 files changed, 3665 insertions(+), 18603 deletions(-) delete mode 100644 .drone.yml delete mode 100644 Cargo.lock delete mode 100644 Cargo.toml delete mode 100644 Makefile delete mode 100644 assets/bootstrap.css delete mode 100644 assets/script.js delete mode 100644 assets/style.css delete mode 100644 assets/ws_debug.js delete mode 100644 docker/dex/dex.config.yaml rename .gitignore => matrixgw_backend/.gitignore (100%) create mode 100644 matrixgw_backend/Cargo.toml rename Dockerfile => matrixgw_backend/Dockerfile (100%) rename docker-compose.yml => matrixgw_backend/docker-compose.yml (95%) rename {docker => matrixgw_backend/docker}/element/config.json (100%) rename {docker => matrixgw_backend/docker}/mas/config.yaml (100%) rename {docker => matrixgw_backend/docker}/synapse/homeserver.yaml (100%) rename {docker => matrixgw_backend/docker}/synapse/localhost.log.config (100%) rename {docker => matrixgw_backend/docker}/synapse/localhost.signing.key (100%) rename {examples => matrixgw_backend/examples}/api_curl.rs (100%) create mode 100644 matrixgw_backend/src/main.rs create mode 100644 matrixgw_frontend/.gitignore create mode 100644 matrixgw_frontend/README.md create mode 100644 matrixgw_frontend/eslint.config.js rename {assets => matrixgw_frontend}/favicon.png (100%) create mode 100644 matrixgw_frontend/index.html create mode 100644 matrixgw_frontend/package-lock.json create mode 100644 matrixgw_frontend/package.json create mode 100644 matrixgw_frontend/public/vite.svg create mode 100644 matrixgw_frontend/src/App.css create mode 100644 matrixgw_frontend/src/App.tsx create mode 100644 matrixgw_frontend/src/assets/react.svg create mode 100644 matrixgw_frontend/src/index.css create mode 100644 matrixgw_frontend/src/main.tsx create mode 100644 matrixgw_frontend/tsconfig.app.json create mode 100644 matrixgw_frontend/tsconfig.json create mode 100644 matrixgw_frontend/tsconfig.node.json create mode 100644 matrixgw_frontend/vite.config.ts delete mode 100644 src/app_config.rs delete mode 100644 src/broadcast_messages.rs delete mode 100644 src/constants.rs delete mode 100644 src/extractors/client_auth.rs delete mode 100644 src/extractors/mod.rs delete mode 100644 src/lib.rs delete mode 100644 src/main.rs delete mode 100644 src/server/api/account.rs delete mode 100644 src/server/api/media.rs delete mode 100644 src/server/api/mod.rs delete mode 100644 src/server/api/profile.rs delete mode 100644 src/server/api/room.rs delete mode 100644 src/server/api/ws.rs delete mode 100644 src/server/mod.rs delete mode 100644 src/server/web_ui.rs delete mode 100644 src/sync_client.rs delete mode 100644 src/user.rs delete mode 100644 src/utils/base_utils.rs delete mode 100644 src/utils/matrix_utils.rs delete mode 100644 src/utils/mod.rs delete mode 100644 templates/base_page.html delete mode 100644 templates/index.html delete mode 100644 templates/ws_debug.html diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 15eb5af..0000000 --- a/.drone.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -kind: pipeline -type: docker -name: default - -steps: - - name: cargo_check - image: rust - commands: - - rustup component add clippy - - cargo clippy -- -D warnings - - cargo test \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index cc2690a..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,4256 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44dfe5c9e0004c623edc65391dfd51daa201e7e30ebd9c9bedf873048ec32bc2" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64 0.22.1", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2 0.3.26", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-remote-ip" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7629b357d4705cf3f1e31f989f48ecd56027112f7d52dcf06dd96ee197065f8e" -dependencies = [ - "actix-web", - "futures-util", - "log", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.8", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-session" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "400c27fd4cdbe0082b7bbd29ac44a3070cbda1b2114138dc106ba39fe2f90dff" -dependencies = [ - "actix-service", - "actix-utils", - "actix-web", - "anyhow", - "derive_more", - "rand 0.9.2", - "redis", - "serde", - "serde_json", - "tracing", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.5.8", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "actix-ws" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99" -dependencies = [ - "actix-codec", - "actix-http", - "actix-web", - "bytestring", - "futures-core", - "tokio", -] - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.6", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" -dependencies = [ - "anstyle", - "once_cell", - "windows-sys 0.59.0", -] - -[[package]] -name = "anyhow" -version = "1.0.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" - -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - -[[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_variant" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38fa22307249f86fb7fad906fcae77f2564caeb56d7209103c551cd1cf4798f" - -[[package]] -name = "askama" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" -dependencies = [ - "askama_derive", - "itoa", - "percent-encoding", - "serde", - "serde_json", -] - -[[package]] -name = "askama_derive" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" -dependencies = [ - "askama_parser", - "basic-toml", - "memchr", - "proc-macro2", - "quote", - "rustc-hash", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "askama_parser" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" -dependencies = [ - "memchr", - "serde", - "serde_derive", - "winnow", -] - -[[package]] -name = "assign" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-trait" -version = "0.1.87" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "attohttpc" -version = "0.28.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07a9b245ba0739fc90935094c29adbaee3f977218b5fb95e822e261cda7f56a3" -dependencies = [ - "http 1.3.1", - "log", - "native-tls", - "serde", - "serde_json", - "url", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "aws-creds" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba912106484991c456adb3364338a2534d0818bd9374b324b608074e3b55f581" -dependencies = [ - "attohttpc", - "home", - "log", - "quick-xml 0.32.0", - "rust-ini", - "serde", - "thiserror 1.0.69", - "time", - "url", -] - -[[package]] -name = "aws-region" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ae4ae7c45238b60af0a3b27ef2fcc7bd5b8fdcd8a6d679919558b40d3eff7a" -dependencies = [ - "thiserror 1.0.69", -] - -[[package]] -name = "backon" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" -dependencies = [ - "fastrand", -] - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base16ct" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b" - -[[package]] -name = "base64" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97d56060ee67d285efb8001fec9d2a4c710c32efd2e14b5cbb5ba71930fc2d" - -[[package]] -name = "basic-toml" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" -dependencies = [ - "serde", -] - -[[package]] -name = "binstring" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed79c2a8151273c70956b5e3cdfdc1ff6c1a8b9779ba59c6807d281b32ee2f86" - -[[package]] -name = "bitflags" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" - -[[package]] -name = "blake2b_simd" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.11.0-rc.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a229bfd78e4827c91b9b95784f69492c1b77c1ab75a45a8a037b139215086f94" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "brotli" -version = "8.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "bytestring" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" -dependencies = [ - "bytes", -] - -[[package]] -name = "castaway" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cc" -version = "1.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" -dependencies = [ - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.6", - "inout", -] - -[[package]] -name = "clap" -version = "4.5.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - -[[package]] -name = "coarsetime" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" -dependencies = [ - "libc", - "wasix", - "wasm-bindgen", -] - -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "compact_str" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "ryu", - "static_assertions", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const-oid" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cb3c4a0d3776f7535c32793be81d6d5fec0d48ac70955d9834e643aa249a52f" - -[[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 0.2.15", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "const_panic" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" - -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "aes-gcm", - "base64 0.20.0", - "hkdf", - "hmac", - "percent-encoding", - "rand 0.8.5", - "sha2 0.10.8", - "subtle", - "time", - "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-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "crypto-common" -version = "0.2.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a23fa214dea9efd4dacee5a5614646b30216ae0f05d4bb51bafb50e9da1c5be" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "ct-codecs" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b916ba8ce9e4182696896f015e8a5ae6081b305f74690baa8465e35f5a142ea4" - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "date_header" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c03c416ed1a30fbb027ef484ba6ab6f80e1eada675e1a2b92fd673c045a1f1d" - -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid 0.9.6", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "const-oid 0.9.6", - "crypto-common 0.1.6", - "subtle", -] - -[[package]] -name = "digest" -version = "0.11.0-rc.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460dd7f37e4950526b54a5a6b1f41b6c8e763c58eb9a8fc8fc05ba5c2f44ca7b" -dependencies = [ - "block-buffer 0.11.0-rc.4", - "const-oid 0.10.0", - "crypto-common 0.2.0-rc.3", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519-compact" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" -dependencies = [ - "ct-codecs", - "getrandom 0.2.15", -] - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct 0.2.0", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "flate2" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.3.1", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hmac-sha1-compact" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3" - -[[package]] -name = "hmac-sha256" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a8575493d277c9092b988c780c94737fb9fd8651a1001e16bee3eccfc1baedb" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hmac-sha512" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b3a0f572aa8389d325f5852b9e0a333a15b0f86ecccbb3fdb6e97cd86dc67c" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.3.1", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hybrid-array" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dab50e193aebe510fe0e40230145820e02f48dae0cf339ea4204e6e708ff7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.8", - "http 1.3.1", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" -dependencies = [ - "futures-util", - "http 1.3.1", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.3.1", - "http-body", - "hyper", - "pin-project-lite", - "socket2 0.5.8", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - -[[package]] -name = "indexmap" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" -dependencies = [ - "equivalent", - "hashbrown 0.15.2", - "serde", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "io-uring" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -dependencies = [ - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jiff" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "js_int" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a" -dependencies = [ - "serde", -] - -[[package]] -name = "js_option" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c" -dependencies = [ - "serde", -] - -[[package]] -name = "jwt-simple" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a" -dependencies = [ - "anyhow", - "binstring", - "blake2b_simd", - "coarsetime", - "ct-codecs", - "ed25519-compact", - "hmac-sha1-compact", - "hmac-sha256", - "hmac-sha512", - "k256", - "p256", - "p384", - "rand 0.8.5", - "serde", - "serde_json", - "superboring", - "thiserror 2.0.16", - "zeroize", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "sha2 0.10.8", - "signature", -] - -[[package]] -name = "konst" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" -dependencies = [ - "const_panic", - "konst_kernel", - "typewit", -] - -[[package]] -name = "konst_kernel" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" -dependencies = [ - "typewit", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "libc" -version = "0.2.174" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" - -[[package]] -name = "libm" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" - -[[package]] -name = "light-openid" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a15777d080e807d5b6b3c0b5a293f7d4680d74a4c66b0cdf9db0441ea9f548" -dependencies = [ - "base64 0.22.1", - "log", - "reqwest", - "serde", - "serde_json", - "urlencoding", -] - -[[package]] -name = "linux-raw-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" - -[[package]] -name = "litemap" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "matrix_gateway" -version = "0.1.0" -dependencies = [ - "actix-remote-ip", - "actix-session", - "actix-web", - "actix-ws", - "anyhow", - "askama", - "base16ct 0.3.0", - "bytes", - "chrono", - "clap", - "env_logger", - "futures-util", - "ipnet", - "jwt-simple", - "lazy_static", - "light-openid", - "log", - "mime_guess", - "rand 0.9.2", - "ruma", - "rust-embed", - "rust-s3", - "serde", - "serde_json", - "sha2 0.11.0-rc.0", - "thiserror 2.0.16", - "tokio", - "urlencoding", - "uuid", -] - -[[package]] -name = "maybe-async" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "md5" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - -[[package]] -name = "minidom" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e394a0e3c7ccc2daea3dffabe82f09857b6b510cb25af87d54bf3e910ac1642d" -dependencies = [ - "rxml", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" -dependencies = [ - "byteorder", - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.5", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openssl" -version = "0.10.71" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "openssl-sys" -version = "0.9.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2 0.10.8", -] - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2 0.10.8", -] - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro-crate" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quick-xml" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.1", -] - -[[package]] -name = "redis" -version = "0.32.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" -dependencies = [ - "arc-swap", - "backon", - "bytes", - "cfg-if", - "combine", - "futures-channel", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "ryu", - "socket2 0.6.0", - "tokio", - "tokio-util", - "url", -] - -[[package]] -name = "redox_syscall" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "reqwest" -version = "0.12.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e327e510263980e231de548a33e63d34962d29ae61b467389a1a09627a254" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.4.8", - "http 1.3.1", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tokio-util", - "tower", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "windows-registry", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rsa" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" -dependencies = [ - "const-oid 0.9.6", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core 0.6.4", - "sha2 0.10.8", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "ruma" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3714d4ebd4314e6510bc64194fcdea1b51fe47898169a08f1bb4912e5c10e2c5" -dependencies = [ - "assign", - "js_int", - "js_option", - "ruma-client", - "ruma-client-api", - "ruma-common", - "ruma-events", - "web-time", -] - -[[package]] -name = "ruma-client" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df765f1917f28ef0bf307b19c2c845be4fc2bb77f76e00b1eafbfa8921f7952" -dependencies = [ - "as_variant", - "assign", - "async-stream", - "bytes", - "futures-core", - "http 1.3.1", - "http-body-util", - "hyper", - "hyper-tls", - "hyper-util", - "ruma-client-api", - "ruma-common", - "serde_html_form", - "tracing", -] - -[[package]] -name = "ruma-client-api" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9e9c613cfda4923b851c5d8bc442305905bee4f0c2b924564b00e71636c8d4" -dependencies = [ - "as_variant", - "assign", - "bytes", - "date_header", - "http 1.3.1", - "js_int", - "js_option", - "maplit", - "ruma-common", - "ruma-events", - "serde", - "serde_html_form", - "serde_json", - "thiserror 2.0.16", - "url", - "web-time", -] - -[[package]] -name = "ruma-common" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387e1898e868d32ff7b205e7db327361d5dcf635c00a8ae5865068607595a9cf" -dependencies = [ - "as_variant", - "base64 0.22.1", - "bytes", - "form_urlencoded", - "getrandom 0.2.15", - "http 1.3.1", - "indexmap", - "js_int", - "konst", - "percent-encoding", - "rand 0.8.5", - "regex", - "ruma-identifiers-validation", - "ruma-macros", - "serde", - "serde_html_form", - "serde_json", - "thiserror 2.0.16", - "time", - "tracing", - "url", - "uuid", - "web-time", - "wildmatch", -] - -[[package]] -name = "ruma-events" -version = "0.30.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f141b37dcd3cfa1199d6a13929db59be529b2c69107edc9f1702b81015e970b2" -dependencies = [ - "as_variant", - "indexmap", - "js_int", - "js_option", - "percent-encoding", - "regex", - "ruma-common", - "ruma-identifiers-validation", - "ruma-macros", - "serde", - "serde_json", - "thiserror 2.0.16", - "tracing", - "url", - "web-time", - "wildmatch", -] - -[[package]] -name = "ruma-identifiers-validation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad674b5e5368c53a2c90fde7dac7e30747004aaf7b1827b72874a25fc06d4d8" -dependencies = [ - "js_int", - "thiserror 2.0.16", -] - -[[package]] -name = "ruma-macros" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ff13fbd6045a7278533390826de316d6116d8582ed828352661337b0c422e1c" -dependencies = [ - "cfg-if", - "proc-macro-crate", - "proc-macro2", - "quote", - "ruma-identifiers-validation", - "serde", - "syn", - "toml", -] - -[[package]] -name = "rust-embed" -version = "8.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" -dependencies = [ - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" -dependencies = [ - "sha2 0.10.8", - "walkdir", -] - -[[package]] -name = "rust-ini" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" -dependencies = [ - "cfg-if", - "ordered-multimap", - "trim-in-place", -] - -[[package]] -name = "rust-s3" -version = "0.36.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7d6f3a3dd397743e8f344ffc80ea7137aee423983ae25b512e5332ad11362f" -dependencies = [ - "async-trait", - "aws-creds", - "aws-region", - "base64 0.22.1", - "bytes", - "cfg-if", - "futures", - "hex", - "hmac", - "http 1.3.1", - "log", - "maybe-async", - "md5", - "minidom", - "percent-encoding", - "quick-xml 0.36.2", - "reqwest", - "serde", - "serde_derive", - "serde_json", - "sha2 0.10.8", - "thiserror 1.0.69", - "time", - "tokio", - "tokio-stream", - "url", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustix" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.23.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" -dependencies = [ - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "rxml" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc94b580d0f5a6b7a2d604e597513d3c673154b52ddeccd1d5c32360d945ee" -dependencies = [ - "bytes", - "rxml_validation", -] - -[[package]] -name = "rxml_validation" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826e80413b9a35e9d33217b3dcac04cf95f6559d15944b93887a08be5496c4a4" -dependencies = [ - "compact_str", -] - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct 0.2.0", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_html_form" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" -dependencies = [ - "form_urlencoded", - "indexmap", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_json" -version = "1.0.143" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_spanned" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.0-rc.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa1d2e6b3cc4e43a8258a9a3b17aa5dfd2cc5186c7024bba8a64aa65b2c71a59" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.11.0-rc.0", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" - -[[package]] -name = "socket2" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "superboring" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" -dependencies = [ - "getrandom 0.2.15", - "hmac-sha256", - "hmac-sha512", - "rand 0.8.5", - "rsa", -] - -[[package]] -name = "syn" -version = "2.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" -dependencies = [ - "cfg-if", - "fastrand", - "getrandom 0.3.1", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" -dependencies = [ - "thiserror-impl 2.0.16", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" - -[[package]] -name = "time-macros" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" -dependencies = [ - "num-conv", - "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 = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2 0.6.0", - "tokio-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", -] - -[[package]] -name = "trim-in-place" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "typewit" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb77c29baba9e4d3a6182d51fa75e3215c7fd1dab8f4ea9d107c716878e55fc0" -dependencies = [ - "typewit_proc_macros", -] - -[[package]] -name = "typewit_proc_macros" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" - -[[package]] -name = "unicase" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.6", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -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.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" -dependencies = [ - "getrandom 0.3.1", - "js-sys", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "wasix" -version = "0.12.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" -dependencies = [ - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "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 = "wildmatch" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-link" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" - -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.53.0", -] - -[[package]] -name = "windows-result" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index dba39d8..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "matrix_gateway" -version = "0.1.0" -edition = "2021" - -[dependencies] -log = "0.4.28" -env_logger = "0.11.8" -clap = { version = "4.5.51", features = ["derive", "env"] } -lazy_static = "1.5.0" -anyhow = "1.0.100" -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.143" -rust-s3 = { version = "0.37.0", features = ["tokio"] } -actix-web = "4.11.0" -actix-session = { version = "0.11.0", features = ["redis-session"] } -light-openid = "1.0.4" -thiserror = "2.0.17" -rand = "0.9.2" -rust-embed = "8.8.0" -mime_guess = "2.0.5" -askama = "0.14.0" -urlencoding = "2.1.3" -uuid = { version = "1.18.0", features = ["v4", "serde"] } -ipnet = { version = "2.11.0", features = ["serde"] } -chrono = "0.4.42" -futures-util = { version = "0.3.31", features = ["sink"] } -jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] } -actix-remote-ip = "0.1.0" -bytes = "1.10.1" -sha2 = "0.11.0-rc.2" -base16ct = { version = "0.3.0", features = ["alloc"] } -ruma = { version = "0.13.0", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] } -actix-ws = "0.3.0" -tokio = { version = "1.48.0", features = ["rt", "time", "macros", "rt-multi-thread"] } diff --git a/Makefile b/Makefile deleted file mode 100644 index 2834da9..0000000 --- a/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -DOCKER_TEMP_DIR=temp - -all: gateway - -gateway: - cargo clippy -- -D warnings && cargo build --release - -gateway_docker: gateway - rm -rf $(DOCKER_TEMP_DIR) - mkdir $(DOCKER_TEMP_DIR) - cp target/release/matrix_gateway $(DOCKER_TEMP_DIR) - docker build -t pierre42100/matrix_gateway -f ./Dockerfile "$(DOCKER_TEMP_DIR)" - rm -rf $(DOCKER_TEMP_DIR) - diff --git a/assets/bootstrap.css b/assets/bootstrap.css deleted file mode 100644 index bcabb7e..0000000 --- a/assets/bootstrap.css +++ /dev/null @@ -1,12199 +0,0 @@ -@charset "UTF-8"; -/*! - * Bootswatch v5.3.3 (https://bootswatch.com) - * Theme: cyborg - * Copyright 2012-2024 Thomas Park - * Licensed under MIT - * Based on Bootstrap -*/ -/*! - * Bootstrap v5.3.3 (https://getbootstrap.com/) - * Copyright 2011-2024 The Bootstrap Authors - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */ -@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"); -:root, -[data-bs-theme=light] { - --bs-blue: #2a9fd6; - --bs-indigo: #6610f2; - --bs-purple: #6f42c1; - --bs-pink: #e83e8c; - --bs-red: #c00; - --bs-orange: #fd7e14; - --bs-yellow: #f80; - --bs-green: #77b300; - --bs-teal: #20c997; - --bs-cyan: #93c; - --bs-black: #000; - --bs-white: #fff; - --bs-gray: #555; - --bs-gray-dark: #222; - --bs-gray-100: #f8f9fa; - --bs-gray-200: #e9ecef; - --bs-gray-300: #dee2e6; - --bs-gray-400: #adafae; - --bs-gray-500: #888; - --bs-gray-600: #555; - --bs-gray-700: #282828; - --bs-gray-800: #222; - --bs-gray-900: #212529; - --bs-primary: #2a9fd6; - --bs-secondary: #555; - --bs-success: #77b300; - --bs-info: #93c; - --bs-warning: #f80; - --bs-danger: #c00; - --bs-light: #222; - --bs-dark: #adafae; - --bs-primary-rgb: 42, 159, 214; - --bs-secondary-rgb: 85, 85, 85; - --bs-success-rgb: 119, 179, 0; - --bs-info-rgb: 153, 51, 204; - --bs-warning-rgb: 255, 136, 0; - --bs-danger-rgb: 204, 0, 0; - --bs-light-rgb: 34, 34, 34; - --bs-dark-rgb: 173, 175, 174; - --bs-primary-text-emphasis: #114056; - --bs-secondary-text-emphasis: #222222; - --bs-success-text-emphasis: #304800; - --bs-info-text-emphasis: #3d1452; - --bs-warning-text-emphasis: #663600; - --bs-danger-text-emphasis: #520000; - --bs-light-text-emphasis: #282828; - --bs-dark-text-emphasis: #282828; - --bs-primary-bg-subtle: #d4ecf7; - --bs-secondary-bg-subtle: #dddddd; - --bs-success-bg-subtle: #e4f0cc; - --bs-info-bg-subtle: #ebd6f5; - --bs-warning-bg-subtle: #ffe7cc; - --bs-danger-bg-subtle: #f5cccc; - --bs-light-bg-subtle: #fcfcfd; - --bs-dark-bg-subtle: #adafae; - --bs-primary-border-subtle: #aad9ef; - --bs-secondary-border-subtle: #bbbbbb; - --bs-success-border-subtle: #c9e199; - --bs-info-border-subtle: #d6adeb; - --bs-warning-border-subtle: #ffcf99; - --bs-danger-border-subtle: #eb9999; - --bs-light-border-subtle: #e9ecef; - --bs-dark-border-subtle: #888; - --bs-white-rgb: 255, 255, 255; - --bs-black-rgb: 0, 0, 0; - --bs-font-sans-serif: Roboto, -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; - --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); - --bs-body-font-family: var(--bs-font-sans-serif); - --bs-body-font-size: 1rem; - --bs-body-font-weight: 400; - --bs-body-line-height: 1.5; - --bs-body-color: #adafae; - --bs-body-color-rgb: 173, 175, 174; - --bs-body-bg: #060606; - --bs-body-bg-rgb: 6, 6, 6; - --bs-emphasis-color: #000; - --bs-emphasis-color-rgb: 0, 0, 0; - --bs-secondary-color: rgba(173, 175, 174, 0.75); - --bs-secondary-color-rgb: 173, 175, 174; - --bs-secondary-bg: #e9ecef; - --bs-secondary-bg-rgb: 233, 236, 239; - --bs-tertiary-color: rgba(173, 175, 174, 0.5); - --bs-tertiary-color-rgb: 173, 175, 174; - --bs-tertiary-bg: #f8f9fa; - --bs-tertiary-bg-rgb: 248, 249, 250; - --bs-heading-color: #fff; - --bs-link-color: #2a9fd6; - --bs-link-color-rgb: 42, 159, 214; - --bs-link-decoration: underline; - --bs-link-hover-color: #227fab; - --bs-link-hover-color-rgb: 34, 127, 171; - --bs-code-color: #e83e8c; - --bs-highlight-color: #adafae; - --bs-highlight-bg: #ffe7cc; - --bs-border-width: 1px; - --bs-border-style: solid; - --bs-border-color: #dee2e6; - --bs-border-color-translucent: rgba(0, 0, 0, 0.175); - --bs-border-radius: 0.375rem; - --bs-border-radius-sm: 0.25rem; - --bs-border-radius-lg: 0.5rem; - --bs-border-radius-xl: 1rem; - --bs-border-radius-xxl: 2rem; - --bs-border-radius-2xl: var(--bs-border-radius-xxl); - --bs-border-radius-pill: 50rem; - --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); - --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); - --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); - --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); - --bs-focus-ring-width: 0.25rem; - --bs-focus-ring-opacity: 0.25; - --bs-focus-ring-color: rgba(42, 159, 214, 0.25); - --bs-form-valid-color: #77b300; - --bs-form-valid-border-color: #77b300; - --bs-form-invalid-color: #c00; - --bs-form-invalid-border-color: #c00; -} - -[data-bs-theme=dark] { - color-scheme: dark; - --bs-body-color: #dee2e6; - --bs-body-color-rgb: 222, 226, 230; - --bs-body-bg: #212529; - --bs-body-bg-rgb: 33, 37, 41; - --bs-emphasis-color: #fff; - --bs-emphasis-color-rgb: 255, 255, 255; - --bs-secondary-color: rgba(222, 226, 230, 0.75); - --bs-secondary-color-rgb: 222, 226, 230; - --bs-secondary-bg: #222; - --bs-secondary-bg-rgb: 34, 34, 34; - --bs-tertiary-color: rgba(222, 226, 230, 0.5); - --bs-tertiary-color-rgb: 222, 226, 230; - --bs-tertiary-bg: #222426; - --bs-tertiary-bg-rgb: 34, 36, 38; - --bs-primary-text-emphasis: #7fc5e6; - --bs-secondary-text-emphasis: #999999; - --bs-success-text-emphasis: #add166; - --bs-info-text-emphasis: #c285e0; - --bs-warning-text-emphasis: #ffb866; - --bs-danger-text-emphasis: #e06666; - --bs-light-text-emphasis: #f8f9fa; - --bs-dark-text-emphasis: #dee2e6; - --bs-primary-bg-subtle: #08202b; - --bs-secondary-bg-subtle: #111111; - --bs-success-bg-subtle: #182400; - --bs-info-bg-subtle: #1f0a29; - --bs-warning-bg-subtle: #331b00; - --bs-danger-bg-subtle: #290000; - --bs-light-bg-subtle: #222; - --bs-dark-bg-subtle: #111111; - --bs-primary-border-subtle: #195f80; - --bs-secondary-border-subtle: #333333; - --bs-success-border-subtle: #476b00; - --bs-info-border-subtle: #5c1f7a; - --bs-warning-border-subtle: #995200; - --bs-danger-border-subtle: #7a0000; - --bs-light-border-subtle: #282828; - --bs-dark-border-subtle: #222; - --bs-heading-color: inherit; - --bs-link-color: #7fc5e6; - --bs-link-hover-color: #99d1eb; - --bs-link-color-rgb: 127, 197, 230; - --bs-link-hover-color-rgb: 153, 209, 235; - --bs-code-color: #f18bba; - --bs-highlight-color: #dee2e6; - --bs-highlight-bg: #663600; - --bs-border-color: #282828; - --bs-border-color-translucent: rgba(255, 255, 255, 0.15); - --bs-form-valid-color: #add166; - --bs-form-valid-border-color: #add166; - --bs-form-invalid-color: #e06666; - --bs-form-invalid-border-color: #e06666; -} - -*, -*::before, -*::after { - box-sizing: border-box; -} - -@media (prefers-reduced-motion: no-preference) { - :root { - scroll-behavior: smooth; - } -} - -body { - margin: 0; - font-family: var(--bs-body-font-family); - font-size: var(--bs-body-font-size); - font-weight: var(--bs-body-font-weight); - line-height: var(--bs-body-line-height); - color: var(--bs-body-color); - text-align: var(--bs-body-text-align); - background-color: var(--bs-body-bg); - -webkit-text-size-adjust: 100%; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} - -hr { - margin: 1rem 0; - color: inherit; - border: 0; - border-top: var(--bs-border-width) solid; - opacity: 0.25; -} - -h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 { - margin-top: 0; - margin-bottom: 0.5rem; - font-weight: 500; - line-height: 1.2; - color: var(--bs-heading-color); -} - -h1, .h1 { - font-size: calc(1.525rem + 3.3vw); -} -@media (min-width: 1200px) { - h1, .h1 { - font-size: 4rem; - } -} - -h2, .h2 { - font-size: calc(1.425rem + 2.1vw); -} -@media (min-width: 1200px) { - h2, .h2 { - font-size: 3rem; - } -} - -h3, .h3 { - font-size: calc(1.375rem + 1.5vw); -} -@media (min-width: 1200px) { - h3, .h3 { - font-size: 2.5rem; - } -} - -h4, .h4 { - font-size: calc(1.325rem + 0.9vw); -} -@media (min-width: 1200px) { - h4, .h4 { - font-size: 2rem; - } -} - -h5, .h5 { - font-size: calc(1.275rem + 0.3vw); -} -@media (min-width: 1200px) { - h5, .h5 { - font-size: 1.5rem; - } -} - -h6, .h6 { - font-size: 1rem; -} - -p { - margin-top: 0; - margin-bottom: 1rem; -} - -abbr[title] { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; - cursor: help; - -webkit-text-decoration-skip-ink: none; - text-decoration-skip-ink: none; -} - -address { - margin-bottom: 1rem; - font-style: normal; - line-height: inherit; -} - -ol, -ul { - padding-left: 2rem; -} - -ol, -ul, -dl { - margin-top: 0; - margin-bottom: 1rem; -} - -ol ol, -ul ul, -ol ul, -ul ol { - margin-bottom: 0; -} - -dt { - font-weight: 700; -} - -dd { - margin-bottom: 0.5rem; - margin-left: 0; -} - -blockquote { - margin: 0 0 1rem; -} - -b, -strong { - font-weight: bolder; -} - -small, .small { - font-size: 0.875em; -} - -mark, .mark { - padding: 0.1875em; - color: var(--bs-highlight-color); - background-color: var(--bs-highlight-bg); -} - -sub, -sup { - position: relative; - font-size: 0.75em; - line-height: 0; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -a { - color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); - text-decoration: underline; -} -a:hover { - --bs-link-color-rgb: var(--bs-link-hover-color-rgb); -} - -a:not([href]):not([class]), a:not([href]):not([class]):hover { - color: inherit; - text-decoration: none; -} - -pre, -code, -kbd, -samp { - font-family: var(--bs-font-monospace); - font-size: 1em; -} - -pre { - display: block; - margin-top: 0; - margin-bottom: 1rem; - overflow: auto; - font-size: 0.875em; - color: inherit; -} -pre code { - font-size: inherit; - color: inherit; - word-break: normal; -} - -code { - font-size: 0.875em; - color: var(--bs-code-color); - word-wrap: break-word; -} -a > code { - color: inherit; -} - -kbd { - padding: 0.1875rem 0.375rem; - font-size: 0.875em; - color: var(--bs-body-bg); - background-color: var(--bs-body-color); - border-radius: 0.25rem; -} -kbd kbd { - padding: 0; - font-size: 1em; -} - -figure { - margin: 0 0 1rem; -} - -img, -svg { - vertical-align: middle; -} - -table { - caption-side: bottom; - border-collapse: collapse; -} - -caption { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - color: var(--bs-secondary-color); - text-align: left; -} - -th { - text-align: inherit; - text-align: -webkit-match-parent; -} - -thead, -tbody, -tfoot, -tr, -td, -th { - border-color: inherit; - border-style: solid; - border-width: 0; -} - -label { - display: inline-block; -} - -button { - border-radius: 0; -} - -button:focus:not(:focus-visible) { - outline: 0; -} - -input, -button, -select, -optgroup, -textarea { - margin: 0; - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -button, -select { - text-transform: none; -} - -[role=button] { - cursor: pointer; -} - -select { - word-wrap: normal; -} -select:disabled { - opacity: 1; -} - -[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { - display: none !important; -} - -button, -[type=button], -[type=reset], -[type=submit] { - -webkit-appearance: button; -} -button:not(:disabled), -[type=button]:not(:disabled), -[type=reset]:not(:disabled), -[type=submit]:not(:disabled) { - cursor: pointer; -} - -::-moz-focus-inner { - padding: 0; - border-style: none; -} - -textarea { - resize: vertical; -} - -fieldset { - min-width: 0; - padding: 0; - margin: 0; - border: 0; -} - -legend { - float: left; - width: 100%; - padding: 0; - margin-bottom: 0.5rem; - font-size: calc(1.275rem + 0.3vw); - line-height: inherit; -} -@media (min-width: 1200px) { - legend { - font-size: 1.5rem; - } -} -legend + * { - clear: left; -} - -::-webkit-datetime-edit-fields-wrapper, -::-webkit-datetime-edit-text, -::-webkit-datetime-edit-minute, -::-webkit-datetime-edit-hour-field, -::-webkit-datetime-edit-day-field, -::-webkit-datetime-edit-month-field, -::-webkit-datetime-edit-year-field { - padding: 0; -} - -::-webkit-inner-spin-button { - height: auto; -} - -[type=search] { - -webkit-appearance: textfield; - outline-offset: -2px; -} - -/* rtl:raw: -[type="tel"], -[type="url"], -[type="email"], -[type="number"] { - direction: ltr; -} -*/ -::-webkit-search-decoration { - -webkit-appearance: none; -} - -::-webkit-color-swatch-wrapper { - padding: 0; -} - -::-webkit-file-upload-button { - font: inherit; - -webkit-appearance: button; -} - -::file-selector-button { - font: inherit; - -webkit-appearance: button; -} - -output { - display: inline-block; -} - -iframe { - border: 0; -} - -summary { - display: list-item; - cursor: pointer; -} - -progress { - vertical-align: baseline; -} - -[hidden] { - display: none !important; -} - -.lead { - font-size: 1.25rem; - font-weight: 300; -} - -.display-1 { - font-size: calc(1.625rem + 4.5vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-1 { - font-size: 5rem; - } -} - -.display-2 { - font-size: calc(1.575rem + 3.9vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-2 { - font-size: 4.5rem; - } -} - -.display-3 { - font-size: calc(1.525rem + 3.3vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-3 { - font-size: 4rem; - } -} - -.display-4 { - font-size: calc(1.475rem + 2.7vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-4 { - font-size: 3.5rem; - } -} - -.display-5 { - font-size: calc(1.425rem + 2.1vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-5 { - font-size: 3rem; - } -} - -.display-6 { - font-size: calc(1.375rem + 1.5vw); - font-weight: 300; - line-height: 1.2; -} -@media (min-width: 1200px) { - .display-6 { - font-size: 2.5rem; - } -} - -.list-unstyled { - padding-left: 0; - list-style: none; -} - -.list-inline { - padding-left: 0; - list-style: none; -} - -.list-inline-item { - display: inline-block; -} -.list-inline-item:not(:last-child) { - margin-right: 0.5rem; -} - -.initialism { - font-size: 0.875em; - text-transform: uppercase; -} - -.blockquote { - margin-bottom: 1rem; - font-size: 1.25rem; -} -.blockquote > :last-child { - margin-bottom: 0; -} - -.blockquote-footer { - margin-top: -1rem; - margin-bottom: 1rem; - font-size: 0.875em; - color: #555; -} -.blockquote-footer::before { - content: "— "; -} - -.img-fluid { - max-width: 100%; - height: auto; -} - -.img-thumbnail { - padding: 0.25rem; - background-color: var(--bs-body-bg); - border: var(--bs-border-width) solid var(--bs-border-color); - border-radius: var(--bs-border-radius); - max-width: 100%; - height: auto; -} - -.figure { - display: inline-block; -} - -.figure-img { - margin-bottom: 0.5rem; - line-height: 1; -} - -.figure-caption { - font-size: 0.875em; - color: var(--bs-secondary-color); -} - -.container, -.container-fluid, -.container-xxl, -.container-xl, -.container-lg, -.container-md, -.container-sm { - --bs-gutter-x: 1.5rem; - --bs-gutter-y: 0; - width: 100%; - padding-right: calc(var(--bs-gutter-x) * 0.5); - padding-left: calc(var(--bs-gutter-x) * 0.5); - margin-right: auto; - margin-left: auto; -} - -@media (min-width: 576px) { - .container-sm, .container { - max-width: 540px; - } -} -@media (min-width: 768px) { - .container-md, .container-sm, .container { - max-width: 720px; - } -} -@media (min-width: 992px) { - .container-lg, .container-md, .container-sm, .container { - max-width: 960px; - } -} -@media (min-width: 1200px) { - .container-xl, .container-lg, .container-md, .container-sm, .container { - max-width: 1140px; - } -} -@media (min-width: 1400px) { - .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container { - max-width: 1320px; - } -} -:root { - --bs-breakpoint-xs: 0; - --bs-breakpoint-sm: 576px; - --bs-breakpoint-md: 768px; - --bs-breakpoint-lg: 992px; - --bs-breakpoint-xl: 1200px; - --bs-breakpoint-xxl: 1400px; -} - -.row { - --bs-gutter-x: 1.5rem; - --bs-gutter-y: 0; - display: flex; - flex-wrap: wrap; - margin-top: calc(-1 * var(--bs-gutter-y)); - margin-right: calc(-0.5 * var(--bs-gutter-x)); - margin-left: calc(-0.5 * var(--bs-gutter-x)); -} -.row > * { - flex-shrink: 0; - width: 100%; - max-width: 100%; - padding-right: calc(var(--bs-gutter-x) * 0.5); - padding-left: calc(var(--bs-gutter-x) * 0.5); - margin-top: var(--bs-gutter-y); -} - -.col { - flex: 1 0 0%; -} - -.row-cols-auto > * { - flex: 0 0 auto; - width: auto; -} - -.row-cols-1 > * { - flex: 0 0 auto; - width: 100%; -} - -.row-cols-2 > * { - flex: 0 0 auto; - width: 50%; -} - -.row-cols-3 > * { - flex: 0 0 auto; - width: 33.33333333%; -} - -.row-cols-4 > * { - flex: 0 0 auto; - width: 25%; -} - -.row-cols-5 > * { - flex: 0 0 auto; - width: 20%; -} - -.row-cols-6 > * { - flex: 0 0 auto; - width: 16.66666667%; -} - -.col-auto { - flex: 0 0 auto; - width: auto; -} - -.col-1 { - flex: 0 0 auto; - width: 8.33333333%; -} - -.col-2 { - flex: 0 0 auto; - width: 16.66666667%; -} - -.col-3 { - flex: 0 0 auto; - width: 25%; -} - -.col-4 { - flex: 0 0 auto; - width: 33.33333333%; -} - -.col-5 { - flex: 0 0 auto; - width: 41.66666667%; -} - -.col-6 { - flex: 0 0 auto; - width: 50%; -} - -.col-7 { - flex: 0 0 auto; - width: 58.33333333%; -} - -.col-8 { - flex: 0 0 auto; - width: 66.66666667%; -} - -.col-9 { - flex: 0 0 auto; - width: 75%; -} - -.col-10 { - flex: 0 0 auto; - width: 83.33333333%; -} - -.col-11 { - flex: 0 0 auto; - width: 91.66666667%; -} - -.col-12 { - flex: 0 0 auto; - width: 100%; -} - -.offset-1 { - margin-left: 8.33333333%; -} - -.offset-2 { - margin-left: 16.66666667%; -} - -.offset-3 { - margin-left: 25%; -} - -.offset-4 { - margin-left: 33.33333333%; -} - -.offset-5 { - margin-left: 41.66666667%; -} - -.offset-6 { - margin-left: 50%; -} - -.offset-7 { - margin-left: 58.33333333%; -} - -.offset-8 { - margin-left: 66.66666667%; -} - -.offset-9 { - margin-left: 75%; -} - -.offset-10 { - margin-left: 83.33333333%; -} - -.offset-11 { - margin-left: 91.66666667%; -} - -.g-0, -.gx-0 { - --bs-gutter-x: 0; -} - -.g-0, -.gy-0 { - --bs-gutter-y: 0; -} - -.g-1, -.gx-1 { - --bs-gutter-x: 0.25rem; -} - -.g-1, -.gy-1 { - --bs-gutter-y: 0.25rem; -} - -.g-2, -.gx-2 { - --bs-gutter-x: 0.5rem; -} - -.g-2, -.gy-2 { - --bs-gutter-y: 0.5rem; -} - -.g-3, -.gx-3 { - --bs-gutter-x: 1rem; -} - -.g-3, -.gy-3 { - --bs-gutter-y: 1rem; -} - -.g-4, -.gx-4 { - --bs-gutter-x: 1.5rem; -} - -.g-4, -.gy-4 { - --bs-gutter-y: 1.5rem; -} - -.g-5, -.gx-5 { - --bs-gutter-x: 3rem; -} - -.g-5, -.gy-5 { - --bs-gutter-y: 3rem; -} - -@media (min-width: 576px) { - .col-sm { - flex: 1 0 0%; - } - .row-cols-sm-auto > * { - flex: 0 0 auto; - width: auto; - } - .row-cols-sm-1 > * { - flex: 0 0 auto; - width: 100%; - } - .row-cols-sm-2 > * { - flex: 0 0 auto; - width: 50%; - } - .row-cols-sm-3 > * { - flex: 0 0 auto; - width: 33.33333333%; - } - .row-cols-sm-4 > * { - flex: 0 0 auto; - width: 25%; - } - .row-cols-sm-5 > * { - flex: 0 0 auto; - width: 20%; - } - .row-cols-sm-6 > * { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-sm-auto { - flex: 0 0 auto; - width: auto; - } - .col-sm-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - .col-sm-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-sm-3 { - flex: 0 0 auto; - width: 25%; - } - .col-sm-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - .col-sm-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - .col-sm-6 { - flex: 0 0 auto; - width: 50%; - } - .col-sm-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - .col-sm-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - .col-sm-9 { - flex: 0 0 auto; - width: 75%; - } - .col-sm-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - .col-sm-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - .col-sm-12 { - flex: 0 0 auto; - width: 100%; - } - .offset-sm-0 { - margin-left: 0; - } - .offset-sm-1 { - margin-left: 8.33333333%; - } - .offset-sm-2 { - margin-left: 16.66666667%; - } - .offset-sm-3 { - margin-left: 25%; - } - .offset-sm-4 { - margin-left: 33.33333333%; - } - .offset-sm-5 { - margin-left: 41.66666667%; - } - .offset-sm-6 { - margin-left: 50%; - } - .offset-sm-7 { - margin-left: 58.33333333%; - } - .offset-sm-8 { - margin-left: 66.66666667%; - } - .offset-sm-9 { - margin-left: 75%; - } - .offset-sm-10 { - margin-left: 83.33333333%; - } - .offset-sm-11 { - margin-left: 91.66666667%; - } - .g-sm-0, - .gx-sm-0 { - --bs-gutter-x: 0; - } - .g-sm-0, - .gy-sm-0 { - --bs-gutter-y: 0; - } - .g-sm-1, - .gx-sm-1 { - --bs-gutter-x: 0.25rem; - } - .g-sm-1, - .gy-sm-1 { - --bs-gutter-y: 0.25rem; - } - .g-sm-2, - .gx-sm-2 { - --bs-gutter-x: 0.5rem; - } - .g-sm-2, - .gy-sm-2 { - --bs-gutter-y: 0.5rem; - } - .g-sm-3, - .gx-sm-3 { - --bs-gutter-x: 1rem; - } - .g-sm-3, - .gy-sm-3 { - --bs-gutter-y: 1rem; - } - .g-sm-4, - .gx-sm-4 { - --bs-gutter-x: 1.5rem; - } - .g-sm-4, - .gy-sm-4 { - --bs-gutter-y: 1.5rem; - } - .g-sm-5, - .gx-sm-5 { - --bs-gutter-x: 3rem; - } - .g-sm-5, - .gy-sm-5 { - --bs-gutter-y: 3rem; - } -} -@media (min-width: 768px) { - .col-md { - flex: 1 0 0%; - } - .row-cols-md-auto > * { - flex: 0 0 auto; - width: auto; - } - .row-cols-md-1 > * { - flex: 0 0 auto; - width: 100%; - } - .row-cols-md-2 > * { - flex: 0 0 auto; - width: 50%; - } - .row-cols-md-3 > * { - flex: 0 0 auto; - width: 33.33333333%; - } - .row-cols-md-4 > * { - flex: 0 0 auto; - width: 25%; - } - .row-cols-md-5 > * { - flex: 0 0 auto; - width: 20%; - } - .row-cols-md-6 > * { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-md-auto { - flex: 0 0 auto; - width: auto; - } - .col-md-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - .col-md-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-md-3 { - flex: 0 0 auto; - width: 25%; - } - .col-md-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - .col-md-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - .col-md-6 { - flex: 0 0 auto; - width: 50%; - } - .col-md-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - .col-md-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - .col-md-9 { - flex: 0 0 auto; - width: 75%; - } - .col-md-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - .col-md-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - .col-md-12 { - flex: 0 0 auto; - width: 100%; - } - .offset-md-0 { - margin-left: 0; - } - .offset-md-1 { - margin-left: 8.33333333%; - } - .offset-md-2 { - margin-left: 16.66666667%; - } - .offset-md-3 { - margin-left: 25%; - } - .offset-md-4 { - margin-left: 33.33333333%; - } - .offset-md-5 { - margin-left: 41.66666667%; - } - .offset-md-6 { - margin-left: 50%; - } - .offset-md-7 { - margin-left: 58.33333333%; - } - .offset-md-8 { - margin-left: 66.66666667%; - } - .offset-md-9 { - margin-left: 75%; - } - .offset-md-10 { - margin-left: 83.33333333%; - } - .offset-md-11 { - margin-left: 91.66666667%; - } - .g-md-0, - .gx-md-0 { - --bs-gutter-x: 0; - } - .g-md-0, - .gy-md-0 { - --bs-gutter-y: 0; - } - .g-md-1, - .gx-md-1 { - --bs-gutter-x: 0.25rem; - } - .g-md-1, - .gy-md-1 { - --bs-gutter-y: 0.25rem; - } - .g-md-2, - .gx-md-2 { - --bs-gutter-x: 0.5rem; - } - .g-md-2, - .gy-md-2 { - --bs-gutter-y: 0.5rem; - } - .g-md-3, - .gx-md-3 { - --bs-gutter-x: 1rem; - } - .g-md-3, - .gy-md-3 { - --bs-gutter-y: 1rem; - } - .g-md-4, - .gx-md-4 { - --bs-gutter-x: 1.5rem; - } - .g-md-4, - .gy-md-4 { - --bs-gutter-y: 1.5rem; - } - .g-md-5, - .gx-md-5 { - --bs-gutter-x: 3rem; - } - .g-md-5, - .gy-md-5 { - --bs-gutter-y: 3rem; - } -} -@media (min-width: 992px) { - .col-lg { - flex: 1 0 0%; - } - .row-cols-lg-auto > * { - flex: 0 0 auto; - width: auto; - } - .row-cols-lg-1 > * { - flex: 0 0 auto; - width: 100%; - } - .row-cols-lg-2 > * { - flex: 0 0 auto; - width: 50%; - } - .row-cols-lg-3 > * { - flex: 0 0 auto; - width: 33.33333333%; - } - .row-cols-lg-4 > * { - flex: 0 0 auto; - width: 25%; - } - .row-cols-lg-5 > * { - flex: 0 0 auto; - width: 20%; - } - .row-cols-lg-6 > * { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-lg-auto { - flex: 0 0 auto; - width: auto; - } - .col-lg-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - .col-lg-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-lg-3 { - flex: 0 0 auto; - width: 25%; - } - .col-lg-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - .col-lg-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - .col-lg-6 { - flex: 0 0 auto; - width: 50%; - } - .col-lg-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - .col-lg-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - .col-lg-9 { - flex: 0 0 auto; - width: 75%; - } - .col-lg-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - .col-lg-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - .col-lg-12 { - flex: 0 0 auto; - width: 100%; - } - .offset-lg-0 { - margin-left: 0; - } - .offset-lg-1 { - margin-left: 8.33333333%; - } - .offset-lg-2 { - margin-left: 16.66666667%; - } - .offset-lg-3 { - margin-left: 25%; - } - .offset-lg-4 { - margin-left: 33.33333333%; - } - .offset-lg-5 { - margin-left: 41.66666667%; - } - .offset-lg-6 { - margin-left: 50%; - } - .offset-lg-7 { - margin-left: 58.33333333%; - } - .offset-lg-8 { - margin-left: 66.66666667%; - } - .offset-lg-9 { - margin-left: 75%; - } - .offset-lg-10 { - margin-left: 83.33333333%; - } - .offset-lg-11 { - margin-left: 91.66666667%; - } - .g-lg-0, - .gx-lg-0 { - --bs-gutter-x: 0; - } - .g-lg-0, - .gy-lg-0 { - --bs-gutter-y: 0; - } - .g-lg-1, - .gx-lg-1 { - --bs-gutter-x: 0.25rem; - } - .g-lg-1, - .gy-lg-1 { - --bs-gutter-y: 0.25rem; - } - .g-lg-2, - .gx-lg-2 { - --bs-gutter-x: 0.5rem; - } - .g-lg-2, - .gy-lg-2 { - --bs-gutter-y: 0.5rem; - } - .g-lg-3, - .gx-lg-3 { - --bs-gutter-x: 1rem; - } - .g-lg-3, - .gy-lg-3 { - --bs-gutter-y: 1rem; - } - .g-lg-4, - .gx-lg-4 { - --bs-gutter-x: 1.5rem; - } - .g-lg-4, - .gy-lg-4 { - --bs-gutter-y: 1.5rem; - } - .g-lg-5, - .gx-lg-5 { - --bs-gutter-x: 3rem; - } - .g-lg-5, - .gy-lg-5 { - --bs-gutter-y: 3rem; - } -} -@media (min-width: 1200px) { - .col-xl { - flex: 1 0 0%; - } - .row-cols-xl-auto > * { - flex: 0 0 auto; - width: auto; - } - .row-cols-xl-1 > * { - flex: 0 0 auto; - width: 100%; - } - .row-cols-xl-2 > * { - flex: 0 0 auto; - width: 50%; - } - .row-cols-xl-3 > * { - flex: 0 0 auto; - width: 33.33333333%; - } - .row-cols-xl-4 > * { - flex: 0 0 auto; - width: 25%; - } - .row-cols-xl-5 > * { - flex: 0 0 auto; - width: 20%; - } - .row-cols-xl-6 > * { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-xl-auto { - flex: 0 0 auto; - width: auto; - } - .col-xl-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - .col-xl-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-xl-3 { - flex: 0 0 auto; - width: 25%; - } - .col-xl-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - .col-xl-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - .col-xl-6 { - flex: 0 0 auto; - width: 50%; - } - .col-xl-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - .col-xl-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - .col-xl-9 { - flex: 0 0 auto; - width: 75%; - } - .col-xl-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - .col-xl-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - .col-xl-12 { - flex: 0 0 auto; - width: 100%; - } - .offset-xl-0 { - margin-left: 0; - } - .offset-xl-1 { - margin-left: 8.33333333%; - } - .offset-xl-2 { - margin-left: 16.66666667%; - } - .offset-xl-3 { - margin-left: 25%; - } - .offset-xl-4 { - margin-left: 33.33333333%; - } - .offset-xl-5 { - margin-left: 41.66666667%; - } - .offset-xl-6 { - margin-left: 50%; - } - .offset-xl-7 { - margin-left: 58.33333333%; - } - .offset-xl-8 { - margin-left: 66.66666667%; - } - .offset-xl-9 { - margin-left: 75%; - } - .offset-xl-10 { - margin-left: 83.33333333%; - } - .offset-xl-11 { - margin-left: 91.66666667%; - } - .g-xl-0, - .gx-xl-0 { - --bs-gutter-x: 0; - } - .g-xl-0, - .gy-xl-0 { - --bs-gutter-y: 0; - } - .g-xl-1, - .gx-xl-1 { - --bs-gutter-x: 0.25rem; - } - .g-xl-1, - .gy-xl-1 { - --bs-gutter-y: 0.25rem; - } - .g-xl-2, - .gx-xl-2 { - --bs-gutter-x: 0.5rem; - } - .g-xl-2, - .gy-xl-2 { - --bs-gutter-y: 0.5rem; - } - .g-xl-3, - .gx-xl-3 { - --bs-gutter-x: 1rem; - } - .g-xl-3, - .gy-xl-3 { - --bs-gutter-y: 1rem; - } - .g-xl-4, - .gx-xl-4 { - --bs-gutter-x: 1.5rem; - } - .g-xl-4, - .gy-xl-4 { - --bs-gutter-y: 1.5rem; - } - .g-xl-5, - .gx-xl-5 { - --bs-gutter-x: 3rem; - } - .g-xl-5, - .gy-xl-5 { - --bs-gutter-y: 3rem; - } -} -@media (min-width: 1400px) { - .col-xxl { - flex: 1 0 0%; - } - .row-cols-xxl-auto > * { - flex: 0 0 auto; - width: auto; - } - .row-cols-xxl-1 > * { - flex: 0 0 auto; - width: 100%; - } - .row-cols-xxl-2 > * { - flex: 0 0 auto; - width: 50%; - } - .row-cols-xxl-3 > * { - flex: 0 0 auto; - width: 33.33333333%; - } - .row-cols-xxl-4 > * { - flex: 0 0 auto; - width: 25%; - } - .row-cols-xxl-5 > * { - flex: 0 0 auto; - width: 20%; - } - .row-cols-xxl-6 > * { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-xxl-auto { - flex: 0 0 auto; - width: auto; - } - .col-xxl-1 { - flex: 0 0 auto; - width: 8.33333333%; - } - .col-xxl-2 { - flex: 0 0 auto; - width: 16.66666667%; - } - .col-xxl-3 { - flex: 0 0 auto; - width: 25%; - } - .col-xxl-4 { - flex: 0 0 auto; - width: 33.33333333%; - } - .col-xxl-5 { - flex: 0 0 auto; - width: 41.66666667%; - } - .col-xxl-6 { - flex: 0 0 auto; - width: 50%; - } - .col-xxl-7 { - flex: 0 0 auto; - width: 58.33333333%; - } - .col-xxl-8 { - flex: 0 0 auto; - width: 66.66666667%; - } - .col-xxl-9 { - flex: 0 0 auto; - width: 75%; - } - .col-xxl-10 { - flex: 0 0 auto; - width: 83.33333333%; - } - .col-xxl-11 { - flex: 0 0 auto; - width: 91.66666667%; - } - .col-xxl-12 { - flex: 0 0 auto; - width: 100%; - } - .offset-xxl-0 { - margin-left: 0; - } - .offset-xxl-1 { - margin-left: 8.33333333%; - } - .offset-xxl-2 { - margin-left: 16.66666667%; - } - .offset-xxl-3 { - margin-left: 25%; - } - .offset-xxl-4 { - margin-left: 33.33333333%; - } - .offset-xxl-5 { - margin-left: 41.66666667%; - } - .offset-xxl-6 { - margin-left: 50%; - } - .offset-xxl-7 { - margin-left: 58.33333333%; - } - .offset-xxl-8 { - margin-left: 66.66666667%; - } - .offset-xxl-9 { - margin-left: 75%; - } - .offset-xxl-10 { - margin-left: 83.33333333%; - } - .offset-xxl-11 { - margin-left: 91.66666667%; - } - .g-xxl-0, - .gx-xxl-0 { - --bs-gutter-x: 0; - } - .g-xxl-0, - .gy-xxl-0 { - --bs-gutter-y: 0; - } - .g-xxl-1, - .gx-xxl-1 { - --bs-gutter-x: 0.25rem; - } - .g-xxl-1, - .gy-xxl-1 { - --bs-gutter-y: 0.25rem; - } - .g-xxl-2, - .gx-xxl-2 { - --bs-gutter-x: 0.5rem; - } - .g-xxl-2, - .gy-xxl-2 { - --bs-gutter-y: 0.5rem; - } - .g-xxl-3, - .gx-xxl-3 { - --bs-gutter-x: 1rem; - } - .g-xxl-3, - .gy-xxl-3 { - --bs-gutter-y: 1rem; - } - .g-xxl-4, - .gx-xxl-4 { - --bs-gutter-x: 1.5rem; - } - .g-xxl-4, - .gy-xxl-4 { - --bs-gutter-y: 1.5rem; - } - .g-xxl-5, - .gx-xxl-5 { - --bs-gutter-x: 3rem; - } - .g-xxl-5, - .gy-xxl-5 { - --bs-gutter-y: 3rem; - } -} -.table { - --bs-table-color-type: initial; - --bs-table-bg-type: initial; - --bs-table-color-state: initial; - --bs-table-bg-state: initial; - --bs-table-color: #fff; - --bs-table-bg: var(--bs-body-bg); - --bs-table-border-color: #282828; - --bs-table-accent-bg: rgba(255, 255, 255, 0.05); - --bs-table-striped-color: #fff; - --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05); - --bs-table-active-color: #fff; - --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1); - --bs-table-hover-color: #fff; - --bs-table-hover-bg: rgba(255, 255, 255, 0.075); - width: 100%; - margin-bottom: 1rem; - vertical-align: top; - border-color: var(--bs-table-border-color); -} -.table > :not(caption) > * > * { - padding: 0.5rem 0.5rem; - color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color))); - background-color: var(--bs-table-bg); - border-bottom-width: var(--bs-border-width); - box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg))); -} -.table > tbody { - vertical-align: inherit; -} -.table > thead { - vertical-align: bottom; -} - -.table-group-divider { - border-top: calc(var(--bs-border-width) * 2) solid currentcolor; -} - -.caption-top { - caption-side: top; -} - -.table-sm > :not(caption) > * > * { - padding: 0.25rem 0.25rem; -} - -.table-bordered > :not(caption) > * { - border-width: var(--bs-border-width) 0; -} -.table-bordered > :not(caption) > * > * { - border-width: 0 var(--bs-border-width); -} - -.table-borderless > :not(caption) > * > * { - border-bottom-width: 0; -} -.table-borderless > :not(:first-child) { - border-top-width: 0; -} - -.table-striped > tbody > tr:nth-of-type(odd) > * { - --bs-table-color-type: var(--bs-table-striped-color); - --bs-table-bg-type: var(--bs-table-striped-bg); -} - -.table-striped-columns > :not(caption) > tr > :nth-child(even) { - --bs-table-color-type: var(--bs-table-striped-color); - --bs-table-bg-type: var(--bs-table-striped-bg); -} - -.table-active { - --bs-table-color-state: var(--bs-table-active-color); - --bs-table-bg-state: var(--bs-table-active-bg); -} - -.table-hover > tbody > tr:hover > * { - --bs-table-color-state: var(--bs-table-hover-color); - --bs-table-bg-state: var(--bs-table-hover-bg); -} - -.table-primary { - --bs-table-color: #fff; - --bs-table-bg: #2a9fd6; - --bs-table-border-color: #55b2de; - --bs-table-striped-bg: #35a4d8; - --bs-table-striped-color: #fff; - --bs-table-active-bg: #3fa9da; - --bs-table-active-color: #fff; - --bs-table-hover-bg: #3aa6d9; - --bs-table-hover-color: #fff; - color: var(--bs-table-color); - border-color: var(--bs-table-border-color); -} - -.table-secondary { - --bs-table-color: #fff; - --bs-table-bg: #555555; - --bs-table-border-color: #777777; - --bs-table-striped-bg: #5e5e5e; - --bs-table-striped-color: #fff; - --bs-table-active-bg: #666666; - --bs-table-active-color: #fff; - --bs-table-hover-bg: #626262; - --bs-table-hover-color: #fff; - color: var(--bs-table-color); - border-color: var(--bs-table-border-color); -} - -.table-success { - --bs-table-color: #fff; - --bs-table-bg: #77b300; - --bs-table-border-color: #92c233; - --bs-table-striped-bg: #7eb70d; - --bs-table-striped-color: #fff; - --bs-table-active-bg: #85bb1a; - --bs-table-active-color: #fff; - --bs-table-hover-bg: #81b913; - --bs-table-hover-color: #fff; - color: var(--bs-table-color); - border-color: var(--bs-table-border-color); -} - -.table-info { - --bs-table-color: #fff; - --bs-table-bg: #9933cc; - --bs-table-border-color: #ad5cd6; - --bs-table-striped-bg: #9e3dcf; - --bs-table-striped-color: #fff; - --bs-table-active-bg: #a347d1; - --bs-table-active-color: #fff; - --bs-table-hover-bg: #a142d0; - --bs-table-hover-color: #fff; - color: var(--bs-table-color); - border-color: var(--bs-table-border-color); -} - -.table-warning { - --bs-table-color: #fff; - --bs-table-bg: #ff8800; - --bs-table-border-color: #ffa033; - --bs-table-striped-bg: #ff8e0d; - --bs-table-striped-color: #fff; - --bs-table-active-bg: #ff941a; - --bs-table-active-color: #000; - --bs-table-hover-bg: #ff9113; - --bs-table-hover-color: #fff; - color: var(--bs-table-color); - border-color: var(--bs-table-border-color); -} - -.table-danger { - --bs-table-color: #fff; - --bs-table-bg: #cc0000; - --bs-table-border-color: #d63333; - --bs-table-striped-bg: #cf0d0d; - --bs-table-striped-color: #fff; - --bs-table-active-bg: #d11a1a; - --bs-table-active-color: #fff; - --bs-table-hover-bg: #d01313; - --bs-table-hover-color: #fff; - color: var(--bs-table-color); - border-color: var(--bs-table-border-color); -} - -.table-light { - --bs-table-color: #fff; - --bs-table-bg: #222; - --bs-table-border-color: #4e4e4e; - --bs-table-striped-bg: #2d2d2d; - --bs-table-striped-color: #fff; - --bs-table-active-bg: #383838; - --bs-table-active-color: #fff; - --bs-table-hover-bg: #333333; - --bs-table-hover-color: #fff; - color: var(--bs-table-color); - border-color: var(--bs-table-border-color); -} - -.table-dark { - --bs-table-color: #000; - --bs-table-bg: #adafae; - --bs-table-border-color: #8a8c8b; - --bs-table-striped-bg: #a4a6a5; - --bs-table-striped-color: #fff; - --bs-table-active-bg: #9c9e9d; - --bs-table-active-color: #fff; - --bs-table-hover-bg: #a0a2a1; - --bs-table-hover-color: #fff; - color: var(--bs-table-color); - border-color: var(--bs-table-border-color); -} - -.table-responsive { - overflow-x: auto; - -webkit-overflow-scrolling: touch; -} - -@media (max-width: 575.98px) { - .table-responsive-sm { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -@media (max-width: 767.98px) { - .table-responsive-md { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -@media (max-width: 991.98px) { - .table-responsive-lg { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -@media (max-width: 1199.98px) { - .table-responsive-xl { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -@media (max-width: 1399.98px) { - .table-responsive-xxl { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } -} -.form-label { - margin-bottom: 0.5rem; -} - -.col-form-label { - padding-top: 0.375rem; - padding-bottom: 0.375rem; - margin-bottom: 0; - font-size: inherit; - line-height: 1.5; -} - -.col-form-label-lg { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - font-size: 1.25rem; -} - -.col-form-label-sm { - padding-top: 0.25rem; - padding-bottom: 0.25rem; - font-size: 0.875rem; -} - -.form-text { - margin-top: 0.25rem; - font-size: 0.875em; - color: var(--bs-secondary-color); -} - -.form-control { - display: block; - width: 100%; - padding: 0.375rem 1rem; - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color: #212529; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-color: #fff; - background-clip: padding-box; - border: 0 solid #fff; - border-radius: var(--bs-border-radius); - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-control { - transition: none; - } -} -.form-control[type=file] { - overflow: hidden; -} -.form-control[type=file]:not(:disabled):not([readonly]) { - cursor: pointer; -} -.form-control:focus { - color: #212529; - background-color: #fff; - border-color: #95cfeb; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(42, 159, 214, 0.25); -} -.form-control::-webkit-date-and-time-value { - min-width: 85px; - height: 1.5em; - margin: 0; -} -.form-control::-webkit-datetime-edit { - display: block; - padding: 0; -} -.form-control::-moz-placeholder { - color: var(--bs-secondary-color); - opacity: 1; -} -.form-control::placeholder { - color: var(--bs-secondary-color); - opacity: 1; -} -.form-control:disabled { - background-color: #adafae; - opacity: 1; -} -.form-control::-webkit-file-upload-button { - padding: 0.375rem 1rem; - margin: -0.375rem -1rem; - -webkit-margin-end: 1rem; - margin-inline-end: 1rem; - color: #fff; - background-color: #282828; - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: 0; - border-radius: 0; - -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -.form-control::file-selector-button { - padding: 0.375rem 1rem; - margin: -0.375rem -1rem; - -webkit-margin-end: 1rem; - margin-inline-end: 1rem; - color: #fff; - background-color: #282828; - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: 0; - border-radius: 0; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-control::-webkit-file-upload-button { - -webkit-transition: none; - transition: none; - } - .form-control::file-selector-button { - transition: none; - } -} -.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { - background-color: #232323; -} -.form-control:hover:not(:disabled):not([readonly])::file-selector-button { - background-color: #232323; -} - -.form-control-plaintext { - display: block; - width: 100%; - padding: 0.375rem 0; - margin-bottom: 0; - line-height: 1.5; - color: var(--bs-body-color); - background-color: transparent; - border: solid transparent; - border-width: 0 0; -} -.form-control-plaintext:focus { - outline: 0; -} -.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { - padding-right: 0; - padding-left: 0; -} - -.form-control-sm { - min-height: calc(1.5em + 0.5rem + calc(0 * 2)); - padding: 0.25rem 0.5rem; - font-size: 0.875rem; - border-radius: var(--bs-border-radius-sm); -} -.form-control-sm::-webkit-file-upload-button { - padding: 0.25rem 0.5rem; - margin: -0.25rem -0.5rem; - -webkit-margin-end: 0.5rem; - margin-inline-end: 0.5rem; -} -.form-control-sm::file-selector-button { - padding: 0.25rem 0.5rem; - margin: -0.25rem -0.5rem; - -webkit-margin-end: 0.5rem; - margin-inline-end: 0.5rem; -} - -.form-control-lg { - min-height: calc(1.5em + 1rem + calc(0 * 2)); - padding: 0.5rem 1rem; - font-size: 1.25rem; - border-radius: var(--bs-border-radius-lg); -} -.form-control-lg::-webkit-file-upload-button { - padding: 0.5rem 1rem; - margin: -0.5rem -1rem; - -webkit-margin-end: 1rem; - margin-inline-end: 1rem; -} -.form-control-lg::file-selector-button { - padding: 0.5rem 1rem; - margin: -0.5rem -1rem; - -webkit-margin-end: 1rem; - margin-inline-end: 1rem; -} - -textarea.form-control { - min-height: calc(1.5em + 0.75rem + calc(0 * 2)); -} -textarea.form-control-sm { - min-height: calc(1.5em + 0.5rem + calc(0 * 2)); -} -textarea.form-control-lg { - min-height: calc(1.5em + 1rem + calc(0 * 2)); -} - -.form-control-color { - width: 3rem; - height: calc(1.5em + 0.75rem + calc(0 * 2)); - padding: 0.375rem; -} -.form-control-color:not(:disabled):not([readonly]) { - cursor: pointer; -} -.form-control-color::-moz-color-swatch { - border: 0 !important; - border-radius: var(--bs-border-radius); -} -.form-control-color::-webkit-color-swatch { - border: 0 !important; - border-radius: var(--bs-border-radius); -} -.form-control-color.form-control-sm { - height: calc(1.5em + 0.5rem + calc(0 * 2)); -} -.form-control-color.form-control-lg { - height: calc(1.5em + 1rem + calc(0 * 2)); -} - -.form-select { - --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23222' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); - display: block; - width: 100%; - padding: 0.375rem 3rem 0.375rem 1rem; - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color: #212529; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-color: #fff; - background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none); - background-repeat: no-repeat; - background-position: right 1rem center; - background-size: 16px 12px; - border: 0 solid #fff; - border-radius: var(--bs-border-radius); - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-select { - transition: none; - } -} -.form-select:focus { - border-color: #95cfeb; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(42, 159, 214, 0.25); -} -.form-select[multiple], .form-select[size]:not([size="1"]) { - padding-right: 1rem; - background-image: none; -} -.form-select:disabled { - background-color: #adafae; -} -.form-select:-moz-focusring { - color: transparent; - text-shadow: 0 0 0 #212529; -} - -.form-select-sm { - padding-top: 0.25rem; - padding-bottom: 0.25rem; - padding-left: 0.5rem; - font-size: 0.875rem; - border-radius: var(--bs-border-radius-sm); -} - -.form-select-lg { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - font-size: 1.25rem; - border-radius: var(--bs-border-radius-lg); -} - -[data-bs-theme=dark] .form-select { - --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); -} - -.form-check { - display: block; - min-height: 1.5rem; - padding-left: 1.5em; - margin-bottom: 0.125rem; -} -.form-check .form-check-input { - float: left; - margin-left: -1.5em; -} - -.form-check-reverse { - padding-right: 1.5em; - padding-left: 0; - text-align: right; -} -.form-check-reverse .form-check-input { - float: right; - margin-right: -1.5em; - margin-left: 0; -} - -.form-check-input { - --bs-form-check-bg: #fff; - flex-shrink: 0; - width: 1em; - height: 1em; - margin-top: 0.25em; - vertical-align: top; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-color: var(--bs-form-check-bg); - background-image: var(--bs-form-check-bg-image); - background-repeat: no-repeat; - background-position: center; - background-size: contain; - border: none; - -webkit-print-color-adjust: exact; - color-adjust: exact; - print-color-adjust: exact; -} -.form-check-input[type=checkbox] { - border-radius: 0.25em; -} -.form-check-input[type=radio] { - border-radius: 50%; -} -.form-check-input:active { - filter: brightness(90%); -} -.form-check-input:focus { - border-color: #95cfeb; - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(42, 159, 214, 0.25); -} -.form-check-input:checked { - background-color: #2a9fd6; - border-color: #2a9fd6; -} -.form-check-input:checked[type=checkbox] { - --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e"); -} -.form-check-input:checked[type=radio] { - --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); -} -.form-check-input[type=checkbox]:indeterminate { - background-color: #2a9fd6; - border-color: #2a9fd6; - --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); -} -.form-check-input:disabled { - pointer-events: none; - filter: none; - opacity: 0.5; -} -.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { - cursor: default; - opacity: 0.5; -} - -.form-switch { - padding-left: 2.5em; -} -.form-switch .form-check-input { - --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); - width: 2em; - margin-left: -2.5em; - background-image: var(--bs-form-switch-bg); - background-position: left center; - border-radius: 2em; - transition: background-position 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-switch .form-check-input { - transition: none; - } -} -.form-switch .form-check-input:focus { - --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2395cfeb'/%3e%3c/svg%3e"); -} -.form-switch .form-check-input:checked { - background-position: right center; - --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); -} -.form-switch.form-check-reverse { - padding-right: 2.5em; - padding-left: 0; -} -.form-switch.form-check-reverse .form-check-input { - margin-right: -2.5em; - margin-left: 0; -} - -.form-check-inline { - display: inline-block; - margin-right: 1rem; -} - -.btn-check { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; -} -.btn-check[disabled] + .btn, .btn-check:disabled + .btn { - pointer-events: none; - filter: none; - opacity: 0.65; -} - -[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) { - --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e"); -} - -.form-range { - width: 100%; - height: 1.5rem; - padding: 0; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-color: transparent; -} -.form-range:focus { - outline: 0; -} -.form-range:focus::-webkit-slider-thumb { - box-shadow: 0 0 0 1px #060606, 0 0 0 0.25rem rgba(42, 159, 214, 0.25); -} -.form-range:focus::-moz-range-thumb { - box-shadow: 0 0 0 1px #060606, 0 0 0 0.25rem rgba(42, 159, 214, 0.25); -} -.form-range::-moz-focus-outer { - border: 0; -} -.form-range::-webkit-slider-thumb { - width: 1rem; - height: 1rem; - margin-top: -0.25rem; - -webkit-appearance: none; - appearance: none; - background-color: #2a9fd6; - border: 0; - border-radius: 1rem; - -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-range::-webkit-slider-thumb { - -webkit-transition: none; - transition: none; - } -} -.form-range::-webkit-slider-thumb:active { - background-color: #bfe2f3; -} -.form-range::-webkit-slider-runnable-track { - width: 100%; - height: 0.5rem; - color: transparent; - cursor: pointer; - background-color: var(--bs-secondary-bg); - border-color: transparent; - border-radius: 1rem; -} -.form-range::-moz-range-thumb { - width: 1rem; - height: 1rem; - -moz-appearance: none; - appearance: none; - background-color: #2a9fd6; - border: 0; - border-radius: 1rem; - -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-range::-moz-range-thumb { - -moz-transition: none; - transition: none; - } -} -.form-range::-moz-range-thumb:active { - background-color: #bfe2f3; -} -.form-range::-moz-range-track { - width: 100%; - height: 0.5rem; - color: transparent; - cursor: pointer; - background-color: var(--bs-secondary-bg); - border-color: transparent; - border-radius: 1rem; -} -.form-range:disabled { - pointer-events: none; -} -.form-range:disabled::-webkit-slider-thumb { - background-color: var(--bs-secondary-color); -} -.form-range:disabled::-moz-range-thumb { - background-color: var(--bs-secondary-color); -} - -.form-floating { - position: relative; -} -.form-floating > .form-control, -.form-floating > .form-control-plaintext, -.form-floating > .form-select { - height: calc(3.5rem + calc(0 * 2)); - min-height: calc(3.5rem + calc(0 * 2)); - line-height: 1.25; -} -.form-floating > label { - position: absolute; - top: 0; - left: 0; - z-index: 2; - height: 100%; - padding: 1rem 1rem; - overflow: hidden; - text-align: start; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: none; - border: 0 solid transparent; - transform-origin: 0 0; - transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-floating > label { - transition: none; - } -} -.form-floating > .form-control, -.form-floating > .form-control-plaintext { - padding: 1rem 1rem; -} -.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder { - color: transparent; -} -.form-floating > .form-control::placeholder, -.form-floating > .form-control-plaintext::placeholder { - color: transparent; -} -.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) { - padding-top: 1.625rem; - padding-bottom: 0.625rem; -} -.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown), -.form-floating > .form-control-plaintext:focus, -.form-floating > .form-control-plaintext:not(:placeholder-shown) { - padding-top: 1.625rem; - padding-bottom: 0.625rem; -} -.form-floating > .form-control:-webkit-autofill, -.form-floating > .form-control-plaintext:-webkit-autofill { - padding-top: 1.625rem; - padding-bottom: 0.625rem; -} -.form-floating > .form-select { - padding-top: 1.625rem; - padding-bottom: 0.625rem; -} -.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label { - color: rgba(var(--bs-body-color-rgb), 0.65); - transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); -} -.form-floating > .form-control:focus ~ label, -.form-floating > .form-control:not(:placeholder-shown) ~ label, -.form-floating > .form-control-plaintext ~ label, -.form-floating > .form-select ~ label { - color: rgba(var(--bs-body-color-rgb), 0.65); - transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); -} -.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label::after { - position: absolute; - inset: 1rem 0.5rem; - z-index: -1; - height: 1.5em; - content: ""; - background-color: #fff; - border-radius: var(--bs-border-radius); -} -.form-floating > .form-control:focus ~ label::after, -.form-floating > .form-control:not(:placeholder-shown) ~ label::after, -.form-floating > .form-control-plaintext ~ label::after, -.form-floating > .form-select ~ label::after { - position: absolute; - inset: 1rem 0.5rem; - z-index: -1; - height: 1.5em; - content: ""; - background-color: #fff; - border-radius: var(--bs-border-radius); -} -.form-floating > .form-control:-webkit-autofill ~ label { - color: rgba(var(--bs-body-color-rgb), 0.65); - transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); -} -.form-floating > .form-control-plaintext ~ label { - border-width: 0 0; -} -.form-floating > :disabled ~ label, -.form-floating > .form-control:disabled ~ label { - color: #555; -} -.form-floating > :disabled ~ label::after, -.form-floating > .form-control:disabled ~ label::after { - background-color: #adafae; -} - -.input-group { - position: relative; - display: flex; - flex-wrap: wrap; - align-items: stretch; - width: 100%; -} -.input-group > .form-control, -.input-group > .form-select, -.input-group > .form-floating { - position: relative; - flex: 1 1 auto; - width: 1%; - min-width: 0; -} -.input-group > .form-control:focus, -.input-group > .form-select:focus, -.input-group > .form-floating:focus-within { - z-index: 5; -} -.input-group .btn { - position: relative; - z-index: 2; -} -.input-group .btn:focus { - z-index: 5; -} - -.input-group-text { - display: flex; - align-items: center; - padding: 0.375rem 1rem; - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color: #fff; - text-align: center; - white-space: nowrap; - background-color: #282828; - border: 0 solid transparent; - border-radius: var(--bs-border-radius); -} - -.input-group-lg > .form-control, -.input-group-lg > .form-select, -.input-group-lg > .input-group-text, -.input-group-lg > .btn { - padding: 0.5rem 1rem; - font-size: 1.25rem; - border-radius: var(--bs-border-radius-lg); -} - -.input-group-sm > .form-control, -.input-group-sm > .form-select, -.input-group-sm > .input-group-text, -.input-group-sm > .btn { - padding: 0.25rem 0.5rem; - font-size: 0.875rem; - border-radius: var(--bs-border-radius-sm); -} - -.input-group-lg > .form-select, -.input-group-sm > .form-select { - padding-right: 4rem; -} - -.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), -.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3), -.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control, -.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), -.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4), -.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control, -.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { - margin-left: calc(0 * -1); - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} -.input-group > .form-floating:not(:first-child) > .form-control, -.input-group > .form-floating:not(:first-child) > .form-select { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.valid-feedback { - display: none; - width: 100%; - margin-top: 0.25rem; - font-size: 0.875em; - color: var(--bs-form-valid-color); -} - -.valid-tooltip { - position: absolute; - top: 100%; - z-index: 5; - display: none; - max-width: 100%; - padding: 0.25rem 0.5rem; - margin-top: 0.1rem; - font-size: 0.875rem; - color: #fff; - background-color: var(--bs-success); - border-radius: var(--bs-border-radius); -} - -.was-validated :valid ~ .valid-feedback, -.was-validated :valid ~ .valid-tooltip, -.is-valid ~ .valid-feedback, -.is-valid ~ .valid-tooltip { - display: block; -} - -.was-validated .form-control:valid, .form-control.is-valid { - border-color: var(--bs-form-valid-border-color); - padding-right: calc(1.5em + 0.75rem); - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2377b300' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right calc(0.375em + 0.1875rem) center; - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} -.was-validated .form-control:valid:focus, .form-control.is-valid:focus { - border-color: var(--bs-form-valid-border-color); - box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); -} - -.was-validated textarea.form-control:valid, textarea.form-control.is-valid { - padding-right: calc(1.5em + 0.75rem); - background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); -} - -.was-validated .form-select:valid, .form-select.is-valid { - border-color: var(--bs-form-valid-border-color); -} -.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { - --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2377b300' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); - padding-right: 5.5rem; - background-position: right 1rem center, center right 3rem; - background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} -.was-validated .form-select:valid:focus, .form-select.is-valid:focus { - border-color: var(--bs-form-valid-border-color); - box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); -} - -.was-validated .form-control-color:valid, .form-control-color.is-valid { - width: calc(3rem + calc(1.5em + 0.75rem)); -} - -.was-validated .form-check-input:valid, .form-check-input.is-valid { - border-color: var(--bs-form-valid-border-color); -} -.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { - background-color: var(--bs-form-valid-color); -} -.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { - box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); -} -.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { - color: var(--bs-form-valid-color); -} - -.form-check-inline .form-check-input ~ .valid-feedback { - margin-left: 0.5em; -} - -.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid, -.was-validated .input-group > .form-select:not(:focus):valid, -.input-group > .form-select:not(:focus).is-valid, -.was-validated .input-group > .form-floating:not(:focus-within):valid, -.input-group > .form-floating:not(:focus-within).is-valid { - z-index: 3; -} - -.invalid-feedback { - display: none; - width: 100%; - margin-top: 0.25rem; - font-size: 0.875em; - color: var(--bs-form-invalid-color); -} - -.invalid-tooltip { - position: absolute; - top: 100%; - z-index: 5; - display: none; - max-width: 100%; - padding: 0.25rem 0.5rem; - margin-top: 0.1rem; - font-size: 0.875rem; - color: #fff; - background-color: var(--bs-danger); - border-radius: var(--bs-border-radius); -} - -.was-validated :invalid ~ .invalid-feedback, -.was-validated :invalid ~ .invalid-tooltip, -.is-invalid ~ .invalid-feedback, -.is-invalid ~ .invalid-tooltip { - display: block; -} - -.was-validated .form-control:invalid, .form-control.is-invalid { - border-color: var(--bs-form-invalid-border-color); - padding-right: calc(1.5em + 0.75rem); - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23c00'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23c00' stroke='none'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right calc(0.375em + 0.1875rem) center; - background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} -.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { - border-color: var(--bs-form-invalid-border-color); - box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); -} - -.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { - padding-right: calc(1.5em + 0.75rem); - background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem); -} - -.was-validated .form-select:invalid, .form-select.is-invalid { - border-color: var(--bs-form-invalid-border-color); -} -.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { - --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23c00'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23c00' stroke='none'/%3e%3c/svg%3e"); - padding-right: 5.5rem; - background-position: right 1rem center, center right 3rem; - background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); -} -.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { - border-color: var(--bs-form-invalid-border-color); - box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); -} - -.was-validated .form-control-color:invalid, .form-control-color.is-invalid { - width: calc(3rem + calc(1.5em + 0.75rem)); -} - -.was-validated .form-check-input:invalid, .form-check-input.is-invalid { - border-color: var(--bs-form-invalid-border-color); -} -.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { - background-color: var(--bs-form-invalid-color); -} -.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { - box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); -} -.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { - color: var(--bs-form-invalid-color); -} - -.form-check-inline .form-check-input ~ .invalid-feedback { - margin-left: 0.5em; -} - -.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid, -.was-validated .input-group > .form-select:not(:focus):invalid, -.input-group > .form-select:not(:focus).is-invalid, -.was-validated .input-group > .form-floating:not(:focus-within):invalid, -.input-group > .form-floating:not(:focus-within).is-invalid { - z-index: 4; -} - -.btn { - --bs-btn-padding-x: 1rem; - --bs-btn-padding-y: 0.375rem; - --bs-btn-font-family: ; - --bs-btn-font-size: 1rem; - --bs-btn-font-weight: 400; - --bs-btn-line-height: 1.5; - --bs-btn-color: var(--bs-body-color); - --bs-btn-bg: transparent; - --bs-btn-border-width: var(--bs-border-width); - --bs-btn-border-color: transparent; - --bs-btn-border-radius: var(--bs-border-radius); - --bs-btn-hover-border-color: transparent; - --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); - --bs-btn-disabled-opacity: 0.65; - --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5); - display: inline-block; - padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x); - font-family: var(--bs-btn-font-family); - font-size: var(--bs-btn-font-size); - font-weight: var(--bs-btn-font-weight); - line-height: var(--bs-btn-line-height); - color: var(--bs-btn-color); - text-align: center; - text-decoration: none; - vertical-align: middle; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - border: var(--bs-btn-border-width) solid var(--bs-btn-border-color); - border-radius: var(--bs-btn-border-radius); - background-color: var(--bs-btn-bg); - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .btn { - transition: none; - } -} -.btn:hover { - color: var(--bs-btn-hover-color); - background-color: var(--bs-btn-hover-bg); - border-color: var(--bs-btn-hover-border-color); -} -.btn-check + .btn:hover { - color: var(--bs-btn-color); - background-color: var(--bs-btn-bg); - border-color: var(--bs-btn-border-color); -} -.btn:focus-visible { - color: var(--bs-btn-hover-color); - background-color: var(--bs-btn-hover-bg); - border-color: var(--bs-btn-hover-border-color); - outline: 0; - box-shadow: var(--bs-btn-focus-box-shadow); -} -.btn-check:focus-visible + .btn { - border-color: var(--bs-btn-hover-border-color); - outline: 0; - box-shadow: var(--bs-btn-focus-box-shadow); -} -.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show { - color: var(--bs-btn-active-color); - background-color: var(--bs-btn-active-bg); - border-color: var(--bs-btn-active-border-color); -} -.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible { - box-shadow: var(--bs-btn-focus-box-shadow); -} -.btn-check:checked:focus-visible + .btn { - box-shadow: var(--bs-btn-focus-box-shadow); -} -.btn:disabled, .btn.disabled, fieldset:disabled .btn { - color: var(--bs-btn-disabled-color); - pointer-events: none; - background-color: var(--bs-btn-disabled-bg); - border-color: var(--bs-btn-disabled-border-color); - opacity: var(--bs-btn-disabled-opacity); -} - -.btn-primary { - --bs-btn-color: #fff; - --bs-btn-bg: #2a9fd6; - --bs-btn-border-color: #2a9fd6; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #2487b6; - --bs-btn-hover-border-color: #227fab; - --bs-btn-focus-shadow-rgb: 74, 173, 220; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #227fab; - --bs-btn-active-border-color: #2077a1; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #2a9fd6; - --bs-btn-disabled-border-color: #2a9fd6; -} - -.btn-secondary { - --bs-btn-color: #fff; - --bs-btn-bg: #555; - --bs-btn-border-color: #555; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #484848; - --bs-btn-hover-border-color: #444444; - --bs-btn-focus-shadow-rgb: 111, 111, 111; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #444444; - --bs-btn-active-border-color: #404040; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #555; - --bs-btn-disabled-border-color: #555; -} - -.btn-success { - --bs-btn-color: #fff; - --bs-btn-bg: #77b300; - --bs-btn-border-color: #77b300; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #659800; - --bs-btn-hover-border-color: #5f8f00; - --bs-btn-focus-shadow-rgb: 139, 190, 38; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #5f8f00; - --bs-btn-active-border-color: #598600; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #77b300; - --bs-btn-disabled-border-color: #77b300; -} - -.btn-info { - --bs-btn-color: #fff; - --bs-btn-bg: #93c; - --bs-btn-border-color: #93c; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #822bad; - --bs-btn-hover-border-color: #7a29a3; - --bs-btn-focus-shadow-rgb: 168, 82, 212; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #7a29a3; - --bs-btn-active-border-color: #732699; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #93c; - --bs-btn-disabled-border-color: #93c; -} - -.btn-warning { - --bs-btn-color: #fff; - --bs-btn-bg: #f80; - --bs-btn-border-color: #f80; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #d97400; - --bs-btn-hover-border-color: #cc6d00; - --bs-btn-focus-shadow-rgb: 255, 154, 38; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #cc6d00; - --bs-btn-active-border-color: #bf6600; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #f80; - --bs-btn-disabled-border-color: #f80; -} - -.btn-danger { - --bs-btn-color: #fff; - --bs-btn-bg: #c00; - --bs-btn-border-color: #c00; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #ad0000; - --bs-btn-hover-border-color: #a30000; - --bs-btn-focus-shadow-rgb: 212, 38, 38; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #a30000; - --bs-btn-active-border-color: #990000; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #c00; - --bs-btn-disabled-border-color: #c00; -} - -.btn-light { - --bs-btn-color: #fff; - --bs-btn-bg: #222; - --bs-btn-border-color: #222; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #1d1d1d; - --bs-btn-hover-border-color: #1b1b1b; - --bs-btn-focus-shadow-rgb: 67, 67, 67; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #1b1b1b; - --bs-btn-active-border-color: #1a1a1a; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #fff; - --bs-btn-disabled-bg: #222; - --bs-btn-disabled-border-color: #222; -} - -.btn-dark { - --bs-btn-color: #000; - --bs-btn-bg: #adafae; - --bs-btn-border-color: #adafae; - --bs-btn-hover-color: #000; - --bs-btn-hover-bg: #b9bbba; - --bs-btn-hover-border-color: #b5b7b6; - --bs-btn-focus-shadow-rgb: 147, 149, 148; - --bs-btn-active-color: #000; - --bs-btn-active-bg: #bdbfbe; - --bs-btn-active-border-color: #b5b7b6; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #000; - --bs-btn-disabled-bg: #adafae; - --bs-btn-disabled-border-color: #adafae; -} - -.btn-outline-primary { - --bs-btn-color: #2a9fd6; - --bs-btn-border-color: #2a9fd6; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #2a9fd6; - --bs-btn-hover-border-color: #2a9fd6; - --bs-btn-focus-shadow-rgb: 42, 159, 214; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #2a9fd6; - --bs-btn-active-border-color: #2a9fd6; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #2a9fd6; - --bs-btn-disabled-bg: transparent; - --bs-btn-disabled-border-color: #2a9fd6; - --bs-gradient: none; -} - -.btn-outline-secondary { - --bs-btn-color: #555; - --bs-btn-border-color: #555; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #555; - --bs-btn-hover-border-color: #555; - --bs-btn-focus-shadow-rgb: 85, 85, 85; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #555; - --bs-btn-active-border-color: #555; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #555; - --bs-btn-disabled-bg: transparent; - --bs-btn-disabled-border-color: #555; - --bs-gradient: none; -} - -.btn-outline-success { - --bs-btn-color: #77b300; - --bs-btn-border-color: #77b300; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #77b300; - --bs-btn-hover-border-color: #77b300; - --bs-btn-focus-shadow-rgb: 119, 179, 0; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #77b300; - --bs-btn-active-border-color: #77b300; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #77b300; - --bs-btn-disabled-bg: transparent; - --bs-btn-disabled-border-color: #77b300; - --bs-gradient: none; -} - -.btn-outline-info { - --bs-btn-color: #93c; - --bs-btn-border-color: #93c; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #93c; - --bs-btn-hover-border-color: #93c; - --bs-btn-focus-shadow-rgb: 153, 51, 204; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #93c; - --bs-btn-active-border-color: #93c; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #93c; - --bs-btn-disabled-bg: transparent; - --bs-btn-disabled-border-color: #93c; - --bs-gradient: none; -} - -.btn-outline-warning { - --bs-btn-color: #f80; - --bs-btn-border-color: #f80; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #f80; - --bs-btn-hover-border-color: #f80; - --bs-btn-focus-shadow-rgb: 255, 136, 0; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #f80; - --bs-btn-active-border-color: #f80; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #f80; - --bs-btn-disabled-bg: transparent; - --bs-btn-disabled-border-color: #f80; - --bs-gradient: none; -} - -.btn-outline-danger { - --bs-btn-color: #c00; - --bs-btn-border-color: #c00; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #c00; - --bs-btn-hover-border-color: #c00; - --bs-btn-focus-shadow-rgb: 204, 0, 0; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #c00; - --bs-btn-active-border-color: #c00; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #c00; - --bs-btn-disabled-bg: transparent; - --bs-btn-disabled-border-color: #c00; - --bs-gradient: none; -} - -.btn-outline-light { - --bs-btn-color: #222; - --bs-btn-border-color: #222; - --bs-btn-hover-color: #fff; - --bs-btn-hover-bg: #222; - --bs-btn-hover-border-color: #222; - --bs-btn-focus-shadow-rgb: 34, 34, 34; - --bs-btn-active-color: #fff; - --bs-btn-active-bg: #222; - --bs-btn-active-border-color: #222; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #222; - --bs-btn-disabled-bg: transparent; - --bs-btn-disabled-border-color: #222; - --bs-gradient: none; -} - -.btn-outline-dark { - --bs-btn-color: #adafae; - --bs-btn-border-color: #adafae; - --bs-btn-hover-color: #000; - --bs-btn-hover-bg: #adafae; - --bs-btn-hover-border-color: #adafae; - --bs-btn-focus-shadow-rgb: 173, 175, 174; - --bs-btn-active-color: #000; - --bs-btn-active-bg: #adafae; - --bs-btn-active-border-color: #adafae; - --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - --bs-btn-disabled-color: #adafae; - --bs-btn-disabled-bg: transparent; - --bs-btn-disabled-border-color: #adafae; - --bs-gradient: none; -} - -.btn-link { - --bs-btn-font-weight: 400; - --bs-btn-color: var(--bs-link-color); - --bs-btn-bg: transparent; - --bs-btn-border-color: transparent; - --bs-btn-hover-color: var(--bs-link-hover-color); - --bs-btn-hover-border-color: transparent; - --bs-btn-active-color: var(--bs-link-hover-color); - --bs-btn-active-border-color: transparent; - --bs-btn-disabled-color: #555; - --bs-btn-disabled-border-color: transparent; - --bs-btn-box-shadow: 0 0 0 #000; - --bs-btn-focus-shadow-rgb: 74, 173, 220; - text-decoration: underline; -} -.btn-link:focus-visible { - color: var(--bs-btn-color); -} -.btn-link:hover { - color: var(--bs-btn-hover-color); -} - -.btn-lg, .btn-group-lg > .btn { - --bs-btn-padding-y: 0.5rem; - --bs-btn-padding-x: 1rem; - --bs-btn-font-size: 1.25rem; - --bs-btn-border-radius: var(--bs-border-radius-lg); -} - -.btn-sm, .btn-group-sm > .btn { - --bs-btn-padding-y: 0.25rem; - --bs-btn-padding-x: 0.5rem; - --bs-btn-font-size: 0.875rem; - --bs-btn-border-radius: var(--bs-border-radius-sm); -} - -.fade { - transition: opacity 0.15s linear; -} -@media (prefers-reduced-motion: reduce) { - .fade { - transition: none; - } -} -.fade:not(.show) { - opacity: 0; -} - -.collapse:not(.show) { - display: none; -} - -.collapsing { - height: 0; - overflow: hidden; - transition: height 0.35s ease; -} -@media (prefers-reduced-motion: reduce) { - .collapsing { - transition: none; - } -} -.collapsing.collapse-horizontal { - width: 0; - height: auto; - transition: width 0.35s ease; -} -@media (prefers-reduced-motion: reduce) { - .collapsing.collapse-horizontal { - transition: none; - } -} - -.dropup, -.dropend, -.dropdown, -.dropstart, -.dropup-center, -.dropdown-center { - position: relative; -} - -.dropdown-toggle { - white-space: nowrap; -} -.dropdown-toggle::after { - display: inline-block; - margin-left: 0.255em; - vertical-align: 0.255em; - content: ""; - border-top: 0.3em solid; - border-right: 0.3em solid transparent; - border-bottom: 0; - border-left: 0.3em solid transparent; -} -.dropdown-toggle:empty::after { - margin-left: 0; -} - -.dropdown-menu { - --bs-dropdown-zindex: 1000; - --bs-dropdown-min-width: 10rem; - --bs-dropdown-padding-x: 0; - --bs-dropdown-padding-y: 0.5rem; - --bs-dropdown-spacer: 0.125rem; - --bs-dropdown-font-size: 1rem; - --bs-dropdown-color: var(--bs-body-color); - --bs-dropdown-bg: #282828; - --bs-dropdown-border-color: var(--bs-border-color-translucent); - --bs-dropdown-border-radius: var(--bs-border-radius); - --bs-dropdown-border-width: var(--bs-border-width); - --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width)); - --bs-dropdown-divider-bg: #222; - --bs-dropdown-divider-margin-y: 0.5rem; - --bs-dropdown-box-shadow: var(--bs-box-shadow); - --bs-dropdown-link-color: #fff; - --bs-dropdown-link-hover-color: #fff; - --bs-dropdown-link-hover-bg: #2a9fd6; - --bs-dropdown-link-active-color: #fff; - --bs-dropdown-link-active-bg: #2a9fd6; - --bs-dropdown-link-disabled-color: var(--bs-tertiary-color); - --bs-dropdown-item-padding-x: 1rem; - --bs-dropdown-item-padding-y: 0.25rem; - --bs-dropdown-header-color: #555; - --bs-dropdown-header-padding-x: 1rem; - --bs-dropdown-header-padding-y: 0.5rem; - position: absolute; - z-index: var(--bs-dropdown-zindex); - display: none; - min-width: var(--bs-dropdown-min-width); - padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x); - margin: 0; - font-size: var(--bs-dropdown-font-size); - color: var(--bs-dropdown-color); - text-align: left; - list-style: none; - background-color: var(--bs-dropdown-bg); - background-clip: padding-box; - border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); - border-radius: var(--bs-dropdown-border-radius); -} -.dropdown-menu[data-bs-popper] { - top: 100%; - left: 0; - margin-top: var(--bs-dropdown-spacer); -} - -.dropdown-menu-start { - --bs-position: start; -} -.dropdown-menu-start[data-bs-popper] { - right: auto; - left: 0; -} - -.dropdown-menu-end { - --bs-position: end; -} -.dropdown-menu-end[data-bs-popper] { - right: 0; - left: auto; -} - -@media (min-width: 576px) { - .dropdown-menu-sm-start { - --bs-position: start; - } - .dropdown-menu-sm-start[data-bs-popper] { - right: auto; - left: 0; - } - .dropdown-menu-sm-end { - --bs-position: end; - } - .dropdown-menu-sm-end[data-bs-popper] { - right: 0; - left: auto; - } -} -@media (min-width: 768px) { - .dropdown-menu-md-start { - --bs-position: start; - } - .dropdown-menu-md-start[data-bs-popper] { - right: auto; - left: 0; - } - .dropdown-menu-md-end { - --bs-position: end; - } - .dropdown-menu-md-end[data-bs-popper] { - right: 0; - left: auto; - } -} -@media (min-width: 992px) { - .dropdown-menu-lg-start { - --bs-position: start; - } - .dropdown-menu-lg-start[data-bs-popper] { - right: auto; - left: 0; - } - .dropdown-menu-lg-end { - --bs-position: end; - } - .dropdown-menu-lg-end[data-bs-popper] { - right: 0; - left: auto; - } -} -@media (min-width: 1200px) { - .dropdown-menu-xl-start { - --bs-position: start; - } - .dropdown-menu-xl-start[data-bs-popper] { - right: auto; - left: 0; - } - .dropdown-menu-xl-end { - --bs-position: end; - } - .dropdown-menu-xl-end[data-bs-popper] { - right: 0; - left: auto; - } -} -@media (min-width: 1400px) { - .dropdown-menu-xxl-start { - --bs-position: start; - } - .dropdown-menu-xxl-start[data-bs-popper] { - right: auto; - left: 0; - } - .dropdown-menu-xxl-end { - --bs-position: end; - } - .dropdown-menu-xxl-end[data-bs-popper] { - right: 0; - left: auto; - } -} -.dropup .dropdown-menu[data-bs-popper] { - top: auto; - bottom: 100%; - margin-top: 0; - margin-bottom: var(--bs-dropdown-spacer); -} -.dropup .dropdown-toggle::after { - display: inline-block; - margin-left: 0.255em; - vertical-align: 0.255em; - content: ""; - border-top: 0; - border-right: 0.3em solid transparent; - border-bottom: 0.3em solid; - border-left: 0.3em solid transparent; -} -.dropup .dropdown-toggle:empty::after { - margin-left: 0; -} - -.dropend .dropdown-menu[data-bs-popper] { - top: 0; - right: auto; - left: 100%; - margin-top: 0; - margin-left: var(--bs-dropdown-spacer); -} -.dropend .dropdown-toggle::after { - display: inline-block; - margin-left: 0.255em; - vertical-align: 0.255em; - content: ""; - border-top: 0.3em solid transparent; - border-right: 0; - border-bottom: 0.3em solid transparent; - border-left: 0.3em solid; -} -.dropend .dropdown-toggle:empty::after { - margin-left: 0; -} -.dropend .dropdown-toggle::after { - vertical-align: 0; -} - -.dropstart .dropdown-menu[data-bs-popper] { - top: 0; - right: 100%; - left: auto; - margin-top: 0; - margin-right: var(--bs-dropdown-spacer); -} -.dropstart .dropdown-toggle::after { - display: inline-block; - margin-left: 0.255em; - vertical-align: 0.255em; - content: ""; -} -.dropstart .dropdown-toggle::after { - display: none; -} -.dropstart .dropdown-toggle::before { - display: inline-block; - margin-right: 0.255em; - vertical-align: 0.255em; - content: ""; - border-top: 0.3em solid transparent; - border-right: 0.3em solid; - border-bottom: 0.3em solid transparent; -} -.dropstart .dropdown-toggle:empty::after { - margin-left: 0; -} -.dropstart .dropdown-toggle::before { - vertical-align: 0; -} - -.dropdown-divider { - height: 0; - margin: var(--bs-dropdown-divider-margin-y) 0; - overflow: hidden; - border-top: 1px solid var(--bs-dropdown-divider-bg); - opacity: 1; -} - -.dropdown-item { - display: block; - width: 100%; - padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); - clear: both; - font-weight: 400; - color: var(--bs-dropdown-link-color); - text-align: inherit; - text-decoration: none; - white-space: nowrap; - background-color: transparent; - border: 0; - border-radius: var(--bs-dropdown-item-border-radius, 0); -} -.dropdown-item:hover, .dropdown-item:focus { - color: var(--bs-dropdown-link-hover-color); - background-color: var(--bs-dropdown-link-hover-bg); -} -.dropdown-item.active, .dropdown-item:active { - color: var(--bs-dropdown-link-active-color); - text-decoration: none; - background-color: var(--bs-dropdown-link-active-bg); -} -.dropdown-item.disabled, .dropdown-item:disabled { - color: var(--bs-dropdown-link-disabled-color); - pointer-events: none; - background-color: transparent; -} - -.dropdown-menu.show { - display: block; -} - -.dropdown-header { - display: block; - padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x); - margin-bottom: 0; - font-size: 0.875rem; - color: var(--bs-dropdown-header-color); - white-space: nowrap; -} - -.dropdown-item-text { - display: block; - padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); - color: var(--bs-dropdown-link-color); -} - -.dropdown-menu-dark { - --bs-dropdown-color: #dee2e6; - --bs-dropdown-bg: #222; - --bs-dropdown-border-color: var(--bs-border-color-translucent); - --bs-dropdown-box-shadow: ; - --bs-dropdown-link-color: #dee2e6; - --bs-dropdown-link-hover-color: #fff; - --bs-dropdown-divider-bg: #222; - --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15); - --bs-dropdown-link-active-color: #fff; - --bs-dropdown-link-active-bg: #2a9fd6; - --bs-dropdown-link-disabled-color: #888; - --bs-dropdown-header-color: #888; -} - -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-flex; - vertical-align: middle; -} -.btn-group > .btn, -.btn-group-vertical > .btn { - position: relative; - flex: 1 1 auto; -} -.btn-group > .btn-check:checked + .btn, -.btn-group > .btn-check:focus + .btn, -.btn-group > .btn:hover, -.btn-group > .btn:focus, -.btn-group > .btn:active, -.btn-group > .btn.active, -.btn-group-vertical > .btn-check:checked + .btn, -.btn-group-vertical > .btn-check:focus + .btn, -.btn-group-vertical > .btn:hover, -.btn-group-vertical > .btn:focus, -.btn-group-vertical > .btn:active, -.btn-group-vertical > .btn.active { - z-index: 1; -} - -.btn-toolbar { - display: flex; - flex-wrap: wrap; - justify-content: flex-start; -} -.btn-toolbar .input-group { - width: auto; -} - -.btn-group { - border-radius: var(--bs-border-radius); -} -.btn-group > :not(.btn-check:first-child) + .btn, -.btn-group > .btn-group:not(:first-child) { - margin-left: calc(var(--bs-border-width) * -1); -} -.btn-group > .btn:not(:last-child):not(.dropdown-toggle), -.btn-group > .btn.dropdown-toggle-split:first-child, -.btn-group > .btn-group:not(:last-child) > .btn { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.btn-group > .btn:nth-child(n+3), -.btn-group > :not(.btn-check) + .btn, -.btn-group > .btn-group:not(:first-child) > .btn { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.dropdown-toggle-split { - padding-right: 0.75rem; - padding-left: 0.75rem; -} -.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after { - margin-left: 0; -} -.dropstart .dropdown-toggle-split::before { - margin-right: 0; -} - -.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split { - padding-right: 0.375rem; - padding-left: 0.375rem; -} - -.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split { - padding-right: 0.75rem; - padding-left: 0.75rem; -} - -.btn-group-vertical { - flex-direction: column; - align-items: flex-start; - justify-content: center; -} -.btn-group-vertical > .btn, -.btn-group-vertical > .btn-group { - width: 100%; -} -.btn-group-vertical > .btn:not(:first-child), -.btn-group-vertical > .btn-group:not(:first-child) { - margin-top: calc(var(--bs-border-width) * -1); -} -.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), -.btn-group-vertical > .btn-group:not(:last-child) > .btn { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn ~ .btn, -.btn-group-vertical > .btn-group:not(:first-child) > .btn { - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.nav { - --bs-nav-link-padding-x: 1rem; - --bs-nav-link-padding-y: 0.5rem; - --bs-nav-link-font-weight: ; - --bs-nav-link-color: var(--bs-link-color); - --bs-nav-link-hover-color: var(--bs-link-hover-color); - --bs-nav-link-disabled-color: var(--bs-secondary-color); - display: flex; - flex-wrap: wrap; - padding-left: 0; - margin-bottom: 0; - list-style: none; -} - -.nav-link { - display: block; - padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x); - font-size: var(--bs-nav-link-font-size); - font-weight: var(--bs-nav-link-font-weight); - color: var(--bs-nav-link-color); - text-decoration: none; - background: none; - border: 0; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .nav-link { - transition: none; - } -} -.nav-link:hover, .nav-link:focus { - color: var(--bs-nav-link-hover-color); -} -.nav-link:focus-visible { - outline: 0; - box-shadow: 0 0 0 0.25rem rgba(42, 159, 214, 0.25); -} -.nav-link.disabled, .nav-link:disabled { - color: var(--bs-nav-link-disabled-color); - pointer-events: none; - cursor: default; -} - -.nav-tabs { - --bs-nav-tabs-border-width: var(--bs-border-width); - --bs-nav-tabs-border-color: #282828; - --bs-nav-tabs-border-radius: var(--bs-border-radius); - --bs-nav-tabs-link-hover-border-color: #282828; - --bs-nav-tabs-link-active-color: #fff; - --bs-nav-tabs-link-active-bg: #282828; - --bs-nav-tabs-link-active-border-color: #282828; - border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color); -} -.nav-tabs .nav-link { - margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width)); - border: var(--bs-nav-tabs-border-width) solid transparent; - border-top-left-radius: var(--bs-nav-tabs-border-radius); - border-top-right-radius: var(--bs-nav-tabs-border-radius); -} -.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { - isolation: isolate; - border-color: var(--bs-nav-tabs-link-hover-border-color); -} -.nav-tabs .nav-link.active, -.nav-tabs .nav-item.show .nav-link { - color: var(--bs-nav-tabs-link-active-color); - background-color: var(--bs-nav-tabs-link-active-bg); - border-color: var(--bs-nav-tabs-link-active-border-color); -} -.nav-tabs .dropdown-menu { - margin-top: calc(-1 * var(--bs-nav-tabs-border-width)); - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.nav-pills { - --bs-nav-pills-border-radius: var(--bs-border-radius); - --bs-nav-pills-link-active-color: #fff; - --bs-nav-pills-link-active-bg: #2a9fd6; -} -.nav-pills .nav-link { - border-radius: var(--bs-nav-pills-border-radius); -} -.nav-pills .nav-link.active, -.nav-pills .show > .nav-link { - color: var(--bs-nav-pills-link-active-color); - background-color: var(--bs-nav-pills-link-active-bg); -} - -.nav-underline { - --bs-nav-underline-gap: 1rem; - --bs-nav-underline-border-width: 0.125rem; - --bs-nav-underline-link-active-color: var(--bs-emphasis-color); - gap: var(--bs-nav-underline-gap); -} -.nav-underline .nav-link { - padding-right: 0; - padding-left: 0; - border-bottom: var(--bs-nav-underline-border-width) solid transparent; -} -.nav-underline .nav-link:hover, .nav-underline .nav-link:focus { - border-bottom-color: currentcolor; -} -.nav-underline .nav-link.active, -.nav-underline .show > .nav-link { - font-weight: 700; - color: var(--bs-nav-underline-link-active-color); - border-bottom-color: currentcolor; -} - -.nav-fill > .nav-link, -.nav-fill .nav-item { - flex: 1 1 auto; - text-align: center; -} - -.nav-justified > .nav-link, -.nav-justified .nav-item { - flex-basis: 0; - flex-grow: 1; - text-align: center; -} - -.nav-fill .nav-item .nav-link, -.nav-justified .nav-item .nav-link { - width: 100%; -} - -.tab-content > .tab-pane { - display: none; -} -.tab-content > .active { - display: block; -} - -.navbar { - --bs-navbar-padding-x: 0; - --bs-navbar-padding-y: 0.5rem; - --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65); - --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8); - --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3); - --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1); - --bs-navbar-brand-padding-y: 0.3125rem; - --bs-navbar-brand-margin-end: 1rem; - --bs-navbar-brand-font-size: 1.25rem; - --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1); - --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1); - --bs-navbar-nav-link-padding-x: 0.5rem; - --bs-navbar-toggler-padding-y: 0.25rem; - --bs-navbar-toggler-padding-x: 0.75rem; - --bs-navbar-toggler-font-size: 1.25rem; - --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28173, 175, 174, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); - --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15); - --bs-navbar-toggler-border-radius: var(--bs-border-radius); - --bs-navbar-toggler-focus-width: 0.25rem; - --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out; - position: relative; - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: space-between; - padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x); -} -.navbar > .container, -.navbar > .container-fluid, -.navbar > .container-sm, -.navbar > .container-md, -.navbar > .container-lg, -.navbar > .container-xl, -.navbar > .container-xxl { - display: flex; - flex-wrap: inherit; - align-items: center; - justify-content: space-between; -} -.navbar-brand { - padding-top: var(--bs-navbar-brand-padding-y); - padding-bottom: var(--bs-navbar-brand-padding-y); - margin-right: var(--bs-navbar-brand-margin-end); - font-size: var(--bs-navbar-brand-font-size); - color: var(--bs-navbar-brand-color); - text-decoration: none; - white-space: nowrap; -} -.navbar-brand:hover, .navbar-brand:focus { - color: var(--bs-navbar-brand-hover-color); -} - -.navbar-nav { - --bs-nav-link-padding-x: 0; - --bs-nav-link-padding-y: 0.5rem; - --bs-nav-link-font-weight: ; - --bs-nav-link-color: var(--bs-navbar-color); - --bs-nav-link-hover-color: var(--bs-navbar-hover-color); - --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color); - display: flex; - flex-direction: column; - padding-left: 0; - margin-bottom: 0; - list-style: none; -} -.navbar-nav .nav-link.active, .navbar-nav .nav-link.show { - color: var(--bs-navbar-active-color); -} -.navbar-nav .dropdown-menu { - position: static; -} - -.navbar-text { - padding-top: 0.5rem; - padding-bottom: 0.5rem; - color: var(--bs-navbar-color); -} -.navbar-text a, -.navbar-text a:hover, -.navbar-text a:focus { - color: var(--bs-navbar-active-color); -} - -.navbar-collapse { - flex-basis: 100%; - flex-grow: 1; - align-items: center; -} - -.navbar-toggler { - padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x); - font-size: var(--bs-navbar-toggler-font-size); - line-height: 1; - color: var(--bs-navbar-color); - background-color: transparent; - border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color); - border-radius: var(--bs-navbar-toggler-border-radius); - transition: var(--bs-navbar-toggler-transition); -} -@media (prefers-reduced-motion: reduce) { - .navbar-toggler { - transition: none; - } -} -.navbar-toggler:hover { - text-decoration: none; -} -.navbar-toggler:focus { - text-decoration: none; - outline: 0; - box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width); -} - -.navbar-toggler-icon { - display: inline-block; - width: 1.5em; - height: 1.5em; - vertical-align: middle; - background-image: var(--bs-navbar-toggler-icon-bg); - background-repeat: no-repeat; - background-position: center; - background-size: 100%; -} - -.navbar-nav-scroll { - max-height: var(--bs-scroll-height, 75vh); - overflow-y: auto; -} - -@media (min-width: 576px) { - .navbar-expand-sm { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-sm .navbar-nav { - flex-direction: row; - } - .navbar-expand-sm .navbar-nav .dropdown-menu { - position: absolute; - } - .navbar-expand-sm .navbar-nav .nav-link { - padding-right: var(--bs-navbar-nav-link-padding-x); - padding-left: var(--bs-navbar-nav-link-padding-x); - } - .navbar-expand-sm .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-sm .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-sm .navbar-toggler { - display: none; - } - .navbar-expand-sm .offcanvas { - position: static; - z-index: auto; - flex-grow: 1; - width: auto !important; - height: auto !important; - visibility: visible !important; - background-color: transparent !important; - border: 0 !important; - transform: none !important; - transition: none; - } - .navbar-expand-sm .offcanvas .offcanvas-header { - display: none; - } - .navbar-expand-sm .offcanvas .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -@media (min-width: 768px) { - .navbar-expand-md { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-md .navbar-nav { - flex-direction: row; - } - .navbar-expand-md .navbar-nav .dropdown-menu { - position: absolute; - } - .navbar-expand-md .navbar-nav .nav-link { - padding-right: var(--bs-navbar-nav-link-padding-x); - padding-left: var(--bs-navbar-nav-link-padding-x); - } - .navbar-expand-md .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-md .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-md .navbar-toggler { - display: none; - } - .navbar-expand-md .offcanvas { - position: static; - z-index: auto; - flex-grow: 1; - width: auto !important; - height: auto !important; - visibility: visible !important; - background-color: transparent !important; - border: 0 !important; - transform: none !important; - transition: none; - } - .navbar-expand-md .offcanvas .offcanvas-header { - display: none; - } - .navbar-expand-md .offcanvas .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -@media (min-width: 992px) { - .navbar-expand-lg { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-lg .navbar-nav { - flex-direction: row; - } - .navbar-expand-lg .navbar-nav .dropdown-menu { - position: absolute; - } - .navbar-expand-lg .navbar-nav .nav-link { - padding-right: var(--bs-navbar-nav-link-padding-x); - padding-left: var(--bs-navbar-nav-link-padding-x); - } - .navbar-expand-lg .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-lg .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-lg .navbar-toggler { - display: none; - } - .navbar-expand-lg .offcanvas { - position: static; - z-index: auto; - flex-grow: 1; - width: auto !important; - height: auto !important; - visibility: visible !important; - background-color: transparent !important; - border: 0 !important; - transform: none !important; - transition: none; - } - .navbar-expand-lg .offcanvas .offcanvas-header { - display: none; - } - .navbar-expand-lg .offcanvas .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -@media (min-width: 1200px) { - .navbar-expand-xl { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-xl .navbar-nav { - flex-direction: row; - } - .navbar-expand-xl .navbar-nav .dropdown-menu { - position: absolute; - } - .navbar-expand-xl .navbar-nav .nav-link { - padding-right: var(--bs-navbar-nav-link-padding-x); - padding-left: var(--bs-navbar-nav-link-padding-x); - } - .navbar-expand-xl .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-xl .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-xl .navbar-toggler { - display: none; - } - .navbar-expand-xl .offcanvas { - position: static; - z-index: auto; - flex-grow: 1; - width: auto !important; - height: auto !important; - visibility: visible !important; - background-color: transparent !important; - border: 0 !important; - transform: none !important; - transition: none; - } - .navbar-expand-xl .offcanvas .offcanvas-header { - display: none; - } - .navbar-expand-xl .offcanvas .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -@media (min-width: 1400px) { - .navbar-expand-xxl { - flex-wrap: nowrap; - justify-content: flex-start; - } - .navbar-expand-xxl .navbar-nav { - flex-direction: row; - } - .navbar-expand-xxl .navbar-nav .dropdown-menu { - position: absolute; - } - .navbar-expand-xxl .navbar-nav .nav-link { - padding-right: var(--bs-navbar-nav-link-padding-x); - padding-left: var(--bs-navbar-nav-link-padding-x); - } - .navbar-expand-xxl .navbar-nav-scroll { - overflow: visible; - } - .navbar-expand-xxl .navbar-collapse { - display: flex !important; - flex-basis: auto; - } - .navbar-expand-xxl .navbar-toggler { - display: none; - } - .navbar-expand-xxl .offcanvas { - position: static; - z-index: auto; - flex-grow: 1; - width: auto !important; - height: auto !important; - visibility: visible !important; - background-color: transparent !important; - border: 0 !important; - transform: none !important; - transition: none; - } - .navbar-expand-xxl .offcanvas .offcanvas-header { - display: none; - } - .navbar-expand-xxl .offcanvas .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - } -} -.navbar-expand { - flex-wrap: nowrap; - justify-content: flex-start; -} -.navbar-expand .navbar-nav { - flex-direction: row; -} -.navbar-expand .navbar-nav .dropdown-menu { - position: absolute; -} -.navbar-expand .navbar-nav .nav-link { - padding-right: var(--bs-navbar-nav-link-padding-x); - padding-left: var(--bs-navbar-nav-link-padding-x); -} -.navbar-expand .navbar-nav-scroll { - overflow: visible; -} -.navbar-expand .navbar-collapse { - display: flex !important; - flex-basis: auto; -} -.navbar-expand .navbar-toggler { - display: none; -} -.navbar-expand .offcanvas { - position: static; - z-index: auto; - flex-grow: 1; - width: auto !important; - height: auto !important; - visibility: visible !important; - background-color: transparent !important; - border: 0 !important; - transform: none !important; - transition: none; -} -.navbar-expand .offcanvas .offcanvas-header { - display: none; -} -.navbar-expand .offcanvas .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; -} - -.navbar-dark, -.navbar[data-bs-theme=dark] { - --bs-navbar-color: rgba(255, 255, 255, 0.55); - --bs-navbar-hover-color: #fff; - --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25); - --bs-navbar-active-color: #fff; - --bs-navbar-brand-color: #fff; - --bs-navbar-brand-hover-color: #fff; - --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1); - --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); -} - -[data-bs-theme=dark] .navbar-toggler-icon { - --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); -} - -.card { - --bs-card-spacer-y: 1rem; - --bs-card-spacer-x: 1rem; - --bs-card-title-spacer-y: 0.5rem; - --bs-card-title-color: ; - --bs-card-subtitle-color: ; - --bs-card-border-width: var(--bs-border-width); - --bs-card-border-color: var(--bs-border-color-translucent); - --bs-card-border-radius: var(--bs-border-radius); - --bs-card-box-shadow: ; - --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); - --bs-card-cap-padding-y: 0.5rem; - --bs-card-cap-padding-x: 1rem; - --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03); - --bs-card-cap-color: ; - --bs-card-height: ; - --bs-card-color: ; - --bs-card-bg: #282828; - --bs-card-img-overlay-padding: 1rem; - --bs-card-group-margin: 0.75rem; - position: relative; - display: flex; - flex-direction: column; - min-width: 0; - height: var(--bs-card-height); - color: var(--bs-body-color); - word-wrap: break-word; - background-color: var(--bs-card-bg); - background-clip: border-box; - border: var(--bs-card-border-width) solid var(--bs-card-border-color); - border-radius: var(--bs-card-border-radius); -} -.card > hr { - margin-right: 0; - margin-left: 0; -} -.card > .list-group { - border-top: inherit; - border-bottom: inherit; -} -.card > .list-group:first-child { - border-top-width: 0; - border-top-left-radius: var(--bs-card-inner-border-radius); - border-top-right-radius: var(--bs-card-inner-border-radius); -} -.card > .list-group:last-child { - border-bottom-width: 0; - border-bottom-right-radius: var(--bs-card-inner-border-radius); - border-bottom-left-radius: var(--bs-card-inner-border-radius); -} -.card > .card-header + .list-group, -.card > .list-group + .card-footer { - border-top: 0; -} - -.card-body { - flex: 1 1 auto; - padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x); - color: var(--bs-card-color); -} - -.card-title { - margin-bottom: var(--bs-card-title-spacer-y); - color: var(--bs-card-title-color); -} - -.card-subtitle { - margin-top: calc(-0.5 * var(--bs-card-title-spacer-y)); - margin-bottom: 0; - color: var(--bs-card-subtitle-color); -} - -.card-text:last-child { - margin-bottom: 0; -} - -.card-link + .card-link { - margin-left: var(--bs-card-spacer-x); -} - -.card-header { - padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); - margin-bottom: 0; - color: var(--bs-card-cap-color); - background-color: var(--bs-card-cap-bg); - border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color); -} -.card-header:first-child { - border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0; -} - -.card-footer { - padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); - color: var(--bs-card-cap-color); - background-color: var(--bs-card-cap-bg); - border-top: var(--bs-card-border-width) solid var(--bs-card-border-color); -} -.card-footer:last-child { - border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius); -} - -.card-header-tabs { - margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); - margin-bottom: calc(-1 * var(--bs-card-cap-padding-y)); - margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); - border-bottom: 0; -} -.card-header-tabs .nav-link.active { - background-color: var(--bs-card-bg); - border-bottom-color: var(--bs-card-bg); -} - -.card-header-pills { - margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); - margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); -} - -.card-img-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - padding: var(--bs-card-img-overlay-padding); - border-radius: var(--bs-card-inner-border-radius); -} - -.card-img, -.card-img-top, -.card-img-bottom { - width: 100%; -} - -.card-img, -.card-img-top { - border-top-left-radius: var(--bs-card-inner-border-radius); - border-top-right-radius: var(--bs-card-inner-border-radius); -} - -.card-img, -.card-img-bottom { - border-bottom-right-radius: var(--bs-card-inner-border-radius); - border-bottom-left-radius: var(--bs-card-inner-border-radius); -} - -.card-group > .card { - margin-bottom: var(--bs-card-group-margin); -} -@media (min-width: 576px) { - .card-group { - display: flex; - flex-flow: row wrap; - } - .card-group > .card { - flex: 1 0 0%; - margin-bottom: 0; - } - .card-group > .card + .card { - margin-left: 0; - border-left: 0; - } - .card-group > .card:not(:last-child) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } - .card-group > .card:not(:last-child) .card-img-top, - .card-group > .card:not(:last-child) .card-header { - border-top-right-radius: 0; - } - .card-group > .card:not(:last-child) .card-img-bottom, - .card-group > .card:not(:last-child) .card-footer { - border-bottom-right-radius: 0; - } - .card-group > .card:not(:first-child) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; - } - .card-group > .card:not(:first-child) .card-img-top, - .card-group > .card:not(:first-child) .card-header { - border-top-left-radius: 0; - } - .card-group > .card:not(:first-child) .card-img-bottom, - .card-group > .card:not(:first-child) .card-footer { - border-bottom-left-radius: 0; - } -} - -.accordion { - --bs-accordion-color: var(--bs-body-color); - --bs-accordion-bg: var(--bs-body-bg); - --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; - --bs-accordion-border-color: var(--bs-border-color); - --bs-accordion-border-width: var(--bs-border-width); - --bs-accordion-border-radius: var(--bs-border-radius); - --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); - --bs-accordion-btn-padding-x: 1.25rem; - --bs-accordion-btn-padding-y: 1rem; - --bs-accordion-btn-color: var(--bs-body-color); - --bs-accordion-btn-bg: var(--bs-accordion-bg); - --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23adafae' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e"); - --bs-accordion-btn-icon-width: 1.25rem; - --bs-accordion-btn-icon-transform: rotate(-180deg); - --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out; - --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23114056' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5L8 11L14 5'/%3e%3c/svg%3e"); - --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(42, 159, 214, 0.25); - --bs-accordion-body-padding-x: 1.25rem; - --bs-accordion-body-padding-y: 1rem; - --bs-accordion-active-color: var(--bs-primary-text-emphasis); - --bs-accordion-active-bg: var(--bs-primary-bg-subtle); -} - -.accordion-button { - position: relative; - display: flex; - align-items: center; - width: 100%; - padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x); - font-size: 1rem; - color: var(--bs-accordion-btn-color); - text-align: left; - background-color: var(--bs-accordion-btn-bg); - border: 0; - border-radius: 0; - overflow-anchor: none; - transition: var(--bs-accordion-transition); -} -@media (prefers-reduced-motion: reduce) { - .accordion-button { - transition: none; - } -} -.accordion-button:not(.collapsed) { - color: var(--bs-accordion-active-color); - background-color: var(--bs-accordion-active-bg); - box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color); -} -.accordion-button:not(.collapsed)::after { - background-image: var(--bs-accordion-btn-active-icon); - transform: var(--bs-accordion-btn-icon-transform); -} -.accordion-button::after { - flex-shrink: 0; - width: var(--bs-accordion-btn-icon-width); - height: var(--bs-accordion-btn-icon-width); - margin-left: auto; - content: ""; - background-image: var(--bs-accordion-btn-icon); - background-repeat: no-repeat; - background-size: var(--bs-accordion-btn-icon-width); - transition: var(--bs-accordion-btn-icon-transition); -} -@media (prefers-reduced-motion: reduce) { - .accordion-button::after { - transition: none; - } -} -.accordion-button:hover { - z-index: 2; -} -.accordion-button:focus { - z-index: 3; - outline: 0; - box-shadow: var(--bs-accordion-btn-focus-box-shadow); -} - -.accordion-header { - margin-bottom: 0; -} - -.accordion-item { - color: var(--bs-accordion-color); - background-color: var(--bs-accordion-bg); - border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color); -} -.accordion-item:first-of-type { - border-top-left-radius: var(--bs-accordion-border-radius); - border-top-right-radius: var(--bs-accordion-border-radius); -} -.accordion-item:first-of-type > .accordion-header .accordion-button { - border-top-left-radius: var(--bs-accordion-inner-border-radius); - border-top-right-radius: var(--bs-accordion-inner-border-radius); -} -.accordion-item:not(:first-of-type) { - border-top: 0; -} -.accordion-item:last-of-type { - border-bottom-right-radius: var(--bs-accordion-border-radius); - border-bottom-left-radius: var(--bs-accordion-border-radius); -} -.accordion-item:last-of-type > .accordion-header .accordion-button.collapsed { - border-bottom-right-radius: var(--bs-accordion-inner-border-radius); - border-bottom-left-radius: var(--bs-accordion-inner-border-radius); -} -.accordion-item:last-of-type > .accordion-collapse { - border-bottom-right-radius: var(--bs-accordion-border-radius); - border-bottom-left-radius: var(--bs-accordion-border-radius); -} - -.accordion-body { - padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x); -} - -.accordion-flush > .accordion-item { - border-right: 0; - border-left: 0; - border-radius: 0; -} -.accordion-flush > .accordion-item:first-child { - border-top: 0; -} -.accordion-flush > .accordion-item:last-child { - border-bottom: 0; -} -.accordion-flush > .accordion-item > .accordion-header .accordion-button, .accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed { - border-radius: 0; -} -.accordion-flush > .accordion-item > .accordion-collapse { - border-radius: 0; -} - -[data-bs-theme=dark] .accordion-button::after { - --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%237fc5e6'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); - --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%237fc5e6'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); -} - -.breadcrumb { - --bs-breadcrumb-padding-x: 0.75rem; - --bs-breadcrumb-padding-y: 0.375rem; - --bs-breadcrumb-margin-bottom: 1rem; - --bs-breadcrumb-bg: #282828; - --bs-breadcrumb-border-radius: 0.25rem; - --bs-breadcrumb-divider-color: var(--bs-secondary-color); - --bs-breadcrumb-item-padding-x: 0.5rem; - --bs-breadcrumb-item-active-color: var(--bs-secondary-color); - display: flex; - flex-wrap: wrap; - padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x); - margin-bottom: var(--bs-breadcrumb-margin-bottom); - font-size: var(--bs-breadcrumb-font-size); - list-style: none; - background-color: var(--bs-breadcrumb-bg); - border-radius: var(--bs-breadcrumb-border-radius); -} - -.breadcrumb-item + .breadcrumb-item { - padding-left: var(--bs-breadcrumb-item-padding-x); -} -.breadcrumb-item + .breadcrumb-item::before { - float: left; - padding-right: var(--bs-breadcrumb-item-padding-x); - color: var(--bs-breadcrumb-divider-color); - content: var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */; -} -.breadcrumb-item.active { - color: var(--bs-breadcrumb-item-active-color); -} - -.pagination { - --bs-pagination-padding-x: 0.75rem; - --bs-pagination-padding-y: 0.375rem; - --bs-pagination-font-size: 1rem; - --bs-pagination-color: #fff; - --bs-pagination-bg: #282828; - --bs-pagination-border-width: var(--bs-border-width); - --bs-pagination-border-color: transparent; - --bs-pagination-border-radius: var(--bs-border-radius); - --bs-pagination-hover-color: #fff; - --bs-pagination-hover-bg: #2a9fd6; - --bs-pagination-hover-border-color: transparent; - --bs-pagination-focus-color: var(--bs-link-hover-color); - --bs-pagination-focus-bg: var(--bs-secondary-bg); - --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(42, 159, 214, 0.25); - --bs-pagination-active-color: #fff; - --bs-pagination-active-bg: #2a9fd6; - --bs-pagination-active-border-color: #2a9fd6; - --bs-pagination-disabled-color: var(--bs-secondary-color); - --bs-pagination-disabled-bg: #282828; - --bs-pagination-disabled-border-color: transparent; - display: flex; - padding-left: 0; - list-style: none; -} - -.page-link { - position: relative; - display: block; - padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x); - font-size: var(--bs-pagination-font-size); - color: var(--bs-pagination-color); - text-decoration: none; - background-color: var(--bs-pagination-bg); - border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color); - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .page-link { - transition: none; - } -} -.page-link:hover { - z-index: 2; - color: var(--bs-pagination-hover-color); - background-color: var(--bs-pagination-hover-bg); - border-color: var(--bs-pagination-hover-border-color); -} -.page-link:focus { - z-index: 3; - color: var(--bs-pagination-focus-color); - background-color: var(--bs-pagination-focus-bg); - outline: 0; - box-shadow: var(--bs-pagination-focus-box-shadow); -} -.page-link.active, .active > .page-link { - z-index: 3; - color: var(--bs-pagination-active-color); - background-color: var(--bs-pagination-active-bg); - border-color: var(--bs-pagination-active-border-color); -} -.page-link.disabled, .disabled > .page-link { - color: var(--bs-pagination-disabled-color); - pointer-events: none; - background-color: var(--bs-pagination-disabled-bg); - border-color: var(--bs-pagination-disabled-border-color); -} - -.page-item:not(:first-child) .page-link { - margin-left: calc(var(--bs-border-width) * -1); -} -.page-item:first-child .page-link { - border-top-left-radius: var(--bs-pagination-border-radius); - border-bottom-left-radius: var(--bs-pagination-border-radius); -} -.page-item:last-child .page-link { - border-top-right-radius: var(--bs-pagination-border-radius); - border-bottom-right-radius: var(--bs-pagination-border-radius); -} - -.pagination-lg { - --bs-pagination-padding-x: 1.5rem; - --bs-pagination-padding-y: 0.75rem; - --bs-pagination-font-size: 1.25rem; - --bs-pagination-border-radius: var(--bs-border-radius-lg); -} - -.pagination-sm { - --bs-pagination-padding-x: 0.5rem; - --bs-pagination-padding-y: 0.25rem; - --bs-pagination-font-size: 0.875rem; - --bs-pagination-border-radius: var(--bs-border-radius-sm); -} - -.badge { - --bs-badge-padding-x: 0.65em; - --bs-badge-padding-y: 0.35em; - --bs-badge-font-size: 0.75em; - --bs-badge-font-weight: 700; - --bs-badge-color: #fff; - --bs-badge-border-radius: var(--bs-border-radius); - display: inline-block; - padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x); - font-size: var(--bs-badge-font-size); - font-weight: var(--bs-badge-font-weight); - line-height: 1; - color: var(--bs-badge-color); - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: var(--bs-badge-border-radius); -} -.badge:empty { - display: none; -} - -.btn .badge { - position: relative; - top: -1px; -} - -.alert { - --bs-alert-bg: transparent; - --bs-alert-padding-x: 1rem; - --bs-alert-padding-y: 1rem; - --bs-alert-margin-bottom: 1rem; - --bs-alert-color: inherit; - --bs-alert-border-color: transparent; - --bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color); - --bs-alert-border-radius: var(--bs-border-radius); - --bs-alert-link-color: inherit; - position: relative; - padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x); - margin-bottom: var(--bs-alert-margin-bottom); - color: var(--bs-alert-color); - background-color: var(--bs-alert-bg); - border: var(--bs-alert-border); - border-radius: var(--bs-alert-border-radius); -} - -.alert-heading { - color: inherit; -} - -.alert-link { - font-weight: 700; - color: var(--bs-alert-link-color); -} - -.alert-dismissible { - padding-right: 3rem; -} -.alert-dismissible .btn-close { - position: absolute; - top: 0; - right: 0; - z-index: 2; - padding: 1.25rem 1rem; -} - -.alert-primary { - --bs-alert-color: var(--bs-primary-text-emphasis); - --bs-alert-bg: var(--bs-primary-bg-subtle); - --bs-alert-border-color: var(--bs-primary-border-subtle); - --bs-alert-link-color: var(--bs-primary-text-emphasis); -} - -.alert-secondary { - --bs-alert-color: var(--bs-secondary-text-emphasis); - --bs-alert-bg: var(--bs-secondary-bg-subtle); - --bs-alert-border-color: var(--bs-secondary-border-subtle); - --bs-alert-link-color: var(--bs-secondary-text-emphasis); -} - -.alert-success { - --bs-alert-color: var(--bs-success-text-emphasis); - --bs-alert-bg: var(--bs-success-bg-subtle); - --bs-alert-border-color: var(--bs-success-border-subtle); - --bs-alert-link-color: var(--bs-success-text-emphasis); -} - -.alert-info { - --bs-alert-color: var(--bs-info-text-emphasis); - --bs-alert-bg: var(--bs-info-bg-subtle); - --bs-alert-border-color: var(--bs-info-border-subtle); - --bs-alert-link-color: var(--bs-info-text-emphasis); -} - -.alert-warning { - --bs-alert-color: var(--bs-warning-text-emphasis); - --bs-alert-bg: var(--bs-warning-bg-subtle); - --bs-alert-border-color: var(--bs-warning-border-subtle); - --bs-alert-link-color: var(--bs-warning-text-emphasis); -} - -.alert-danger { - --bs-alert-color: var(--bs-danger-text-emphasis); - --bs-alert-bg: var(--bs-danger-bg-subtle); - --bs-alert-border-color: var(--bs-danger-border-subtle); - --bs-alert-link-color: var(--bs-danger-text-emphasis); -} - -.alert-light { - --bs-alert-color: var(--bs-light-text-emphasis); - --bs-alert-bg: var(--bs-light-bg-subtle); - --bs-alert-border-color: var(--bs-light-border-subtle); - --bs-alert-link-color: var(--bs-light-text-emphasis); -} - -.alert-dark { - --bs-alert-color: var(--bs-dark-text-emphasis); - --bs-alert-bg: var(--bs-dark-bg-subtle); - --bs-alert-border-color: var(--bs-dark-border-subtle); - --bs-alert-link-color: var(--bs-dark-text-emphasis); -} - -@keyframes progress-bar-stripes { - 0% { - background-position-x: 1rem; - } -} -.progress, -.progress-stacked { - --bs-progress-height: 1rem; - --bs-progress-font-size: 0.75rem; - --bs-progress-bg: #282828; - --bs-progress-border-radius: var(--bs-border-radius); - --bs-progress-box-shadow: var(--bs-box-shadow-inset); - --bs-progress-bar-color: #fff; - --bs-progress-bar-bg: #2a9fd6; - --bs-progress-bar-transition: width 0.6s ease; - display: flex; - height: var(--bs-progress-height); - overflow: hidden; - font-size: var(--bs-progress-font-size); - background-color: var(--bs-progress-bg); - border-radius: var(--bs-progress-border-radius); -} - -.progress-bar { - display: flex; - flex-direction: column; - justify-content: center; - overflow: hidden; - color: var(--bs-progress-bar-color); - text-align: center; - white-space: nowrap; - background-color: var(--bs-progress-bar-bg); - transition: var(--bs-progress-bar-transition); -} -@media (prefers-reduced-motion: reduce) { - .progress-bar { - transition: none; - } -} - -.progress-bar-striped { - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-size: var(--bs-progress-height) var(--bs-progress-height); -} - -.progress-stacked > .progress { - overflow: visible; -} - -.progress-stacked > .progress > .progress-bar { - width: 100%; -} - -.progress-bar-animated { - animation: 1s linear infinite progress-bar-stripes; -} -@media (prefers-reduced-motion: reduce) { - .progress-bar-animated { - animation: none; - } -} - -.list-group { - --bs-list-group-color: #fff; - --bs-list-group-bg: #222; - --bs-list-group-border-color: #282828; - --bs-list-group-border-width: var(--bs-border-width); - --bs-list-group-border-radius: var(--bs-border-radius); - --bs-list-group-item-padding-x: 1rem; - --bs-list-group-item-padding-y: 0.5rem; - --bs-list-group-action-color: #fff; - --bs-list-group-action-hover-color: var(--bs-emphasis-color); - --bs-list-group-action-hover-bg: #2a9fd6; - --bs-list-group-action-active-color: var(--bs-body-color); - --bs-list-group-action-active-bg: #2a9fd6; - --bs-list-group-disabled-color: var(--bs-secondary-color); - --bs-list-group-disabled-bg: #282828; - --bs-list-group-active-color: #fff; - --bs-list-group-active-bg: #2a9fd6; - --bs-list-group-active-border-color: #2a9fd6; - display: flex; - flex-direction: column; - padding-left: 0; - margin-bottom: 0; - border-radius: var(--bs-list-group-border-radius); -} - -.list-group-numbered { - list-style-type: none; - counter-reset: section; -} -.list-group-numbered > .list-group-item::before { - content: counters(section, ".") ". "; - counter-increment: section; -} - -.list-group-item-action { - width: 100%; - color: var(--bs-list-group-action-color); - text-align: inherit; -} -.list-group-item-action:hover, .list-group-item-action:focus { - z-index: 1; - color: var(--bs-list-group-action-hover-color); - text-decoration: none; - background-color: var(--bs-list-group-action-hover-bg); -} -.list-group-item-action:active { - color: var(--bs-list-group-action-active-color); - background-color: var(--bs-list-group-action-active-bg); -} - -.list-group-item { - position: relative; - display: block; - padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x); - color: var(--bs-list-group-color); - text-decoration: none; - background-color: var(--bs-list-group-bg); - border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color); -} -.list-group-item:first-child { - border-top-left-radius: inherit; - border-top-right-radius: inherit; -} -.list-group-item:last-child { - border-bottom-right-radius: inherit; - border-bottom-left-radius: inherit; -} -.list-group-item.disabled, .list-group-item:disabled { - color: var(--bs-list-group-disabled-color); - pointer-events: none; - background-color: var(--bs-list-group-disabled-bg); -} -.list-group-item.active { - z-index: 2; - color: var(--bs-list-group-active-color); - background-color: var(--bs-list-group-active-bg); - border-color: var(--bs-list-group-active-border-color); -} -.list-group-item + .list-group-item { - border-top-width: 0; -} -.list-group-item + .list-group-item.active { - margin-top: calc(-1 * var(--bs-list-group-border-width)); - border-top-width: var(--bs-list-group-border-width); -} - -.list-group-horizontal { - flex-direction: row; -} -.list-group-horizontal > .list-group-item:first-child:not(:last-child) { - border-bottom-left-radius: var(--bs-list-group-border-radius); - border-top-right-radius: 0; -} -.list-group-horizontal > .list-group-item:last-child:not(:first-child) { - border-top-right-radius: var(--bs-list-group-border-radius); - border-bottom-left-radius: 0; -} -.list-group-horizontal > .list-group-item.active { - margin-top: 0; -} -.list-group-horizontal > .list-group-item + .list-group-item { - border-top-width: var(--bs-list-group-border-width); - border-left-width: 0; -} -.list-group-horizontal > .list-group-item + .list-group-item.active { - margin-left: calc(-1 * var(--bs-list-group-border-width)); - border-left-width: var(--bs-list-group-border-width); -} - -@media (min-width: 576px) { - .list-group-horizontal-sm { - flex-direction: row; - } - .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) { - border-bottom-left-radius: var(--bs-list-group-border-radius); - border-top-right-radius: 0; - } - .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) { - border-top-right-radius: var(--bs-list-group-border-radius); - border-bottom-left-radius: 0; - } - .list-group-horizontal-sm > .list-group-item.active { - margin-top: 0; - } - .list-group-horizontal-sm > .list-group-item + .list-group-item { - border-top-width: var(--bs-list-group-border-width); - border-left-width: 0; - } - .list-group-horizontal-sm > .list-group-item + .list-group-item.active { - margin-left: calc(-1 * var(--bs-list-group-border-width)); - border-left-width: var(--bs-list-group-border-width); - } -} -@media (min-width: 768px) { - .list-group-horizontal-md { - flex-direction: row; - } - .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) { - border-bottom-left-radius: var(--bs-list-group-border-radius); - border-top-right-radius: 0; - } - .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) { - border-top-right-radius: var(--bs-list-group-border-radius); - border-bottom-left-radius: 0; - } - .list-group-horizontal-md > .list-group-item.active { - margin-top: 0; - } - .list-group-horizontal-md > .list-group-item + .list-group-item { - border-top-width: var(--bs-list-group-border-width); - border-left-width: 0; - } - .list-group-horizontal-md > .list-group-item + .list-group-item.active { - margin-left: calc(-1 * var(--bs-list-group-border-width)); - border-left-width: var(--bs-list-group-border-width); - } -} -@media (min-width: 992px) { - .list-group-horizontal-lg { - flex-direction: row; - } - .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) { - border-bottom-left-radius: var(--bs-list-group-border-radius); - border-top-right-radius: 0; - } - .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) { - border-top-right-radius: var(--bs-list-group-border-radius); - border-bottom-left-radius: 0; - } - .list-group-horizontal-lg > .list-group-item.active { - margin-top: 0; - } - .list-group-horizontal-lg > .list-group-item + .list-group-item { - border-top-width: var(--bs-list-group-border-width); - border-left-width: 0; - } - .list-group-horizontal-lg > .list-group-item + .list-group-item.active { - margin-left: calc(-1 * var(--bs-list-group-border-width)); - border-left-width: var(--bs-list-group-border-width); - } -} -@media (min-width: 1200px) { - .list-group-horizontal-xl { - flex-direction: row; - } - .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) { - border-bottom-left-radius: var(--bs-list-group-border-radius); - border-top-right-radius: 0; - } - .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) { - border-top-right-radius: var(--bs-list-group-border-radius); - border-bottom-left-radius: 0; - } - .list-group-horizontal-xl > .list-group-item.active { - margin-top: 0; - } - .list-group-horizontal-xl > .list-group-item + .list-group-item { - border-top-width: var(--bs-list-group-border-width); - border-left-width: 0; - } - .list-group-horizontal-xl > .list-group-item + .list-group-item.active { - margin-left: calc(-1 * var(--bs-list-group-border-width)); - border-left-width: var(--bs-list-group-border-width); - } -} -@media (min-width: 1400px) { - .list-group-horizontal-xxl { - flex-direction: row; - } - .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) { - border-bottom-left-radius: var(--bs-list-group-border-radius); - border-top-right-radius: 0; - } - .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) { - border-top-right-radius: var(--bs-list-group-border-radius); - border-bottom-left-radius: 0; - } - .list-group-horizontal-xxl > .list-group-item.active { - margin-top: 0; - } - .list-group-horizontal-xxl > .list-group-item + .list-group-item { - border-top-width: var(--bs-list-group-border-width); - border-left-width: 0; - } - .list-group-horizontal-xxl > .list-group-item + .list-group-item.active { - margin-left: calc(-1 * var(--bs-list-group-border-width)); - border-left-width: var(--bs-list-group-border-width); - } -} -.list-group-flush { - border-radius: 0; -} -.list-group-flush > .list-group-item { - border-width: 0 0 var(--bs-list-group-border-width); -} -.list-group-flush > .list-group-item:last-child { - border-bottom-width: 0; -} - -.list-group-item-primary { - --bs-list-group-color: var(--bs-primary-text-emphasis); - --bs-list-group-bg: var(--bs-primary-bg-subtle); - --bs-list-group-border-color: var(--bs-primary-border-subtle); - --bs-list-group-action-hover-color: var(--bs-emphasis-color); - --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle); - --bs-list-group-action-active-color: var(--bs-emphasis-color); - --bs-list-group-action-active-bg: var(--bs-primary-border-subtle); - --bs-list-group-active-color: var(--bs-primary-bg-subtle); - --bs-list-group-active-bg: var(--bs-primary-text-emphasis); - --bs-list-group-active-border-color: var(--bs-primary-text-emphasis); -} - -.list-group-item-secondary { - --bs-list-group-color: var(--bs-secondary-text-emphasis); - --bs-list-group-bg: var(--bs-secondary-bg-subtle); - --bs-list-group-border-color: var(--bs-secondary-border-subtle); - --bs-list-group-action-hover-color: var(--bs-emphasis-color); - --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle); - --bs-list-group-action-active-color: var(--bs-emphasis-color); - --bs-list-group-action-active-bg: var(--bs-secondary-border-subtle); - --bs-list-group-active-color: var(--bs-secondary-bg-subtle); - --bs-list-group-active-bg: var(--bs-secondary-text-emphasis); - --bs-list-group-active-border-color: var(--bs-secondary-text-emphasis); -} - -.list-group-item-success { - --bs-list-group-color: var(--bs-success-text-emphasis); - --bs-list-group-bg: var(--bs-success-bg-subtle); - --bs-list-group-border-color: var(--bs-success-border-subtle); - --bs-list-group-action-hover-color: var(--bs-emphasis-color); - --bs-list-group-action-hover-bg: var(--bs-success-border-subtle); - --bs-list-group-action-active-color: var(--bs-emphasis-color); - --bs-list-group-action-active-bg: var(--bs-success-border-subtle); - --bs-list-group-active-color: var(--bs-success-bg-subtle); - --bs-list-group-active-bg: var(--bs-success-text-emphasis); - --bs-list-group-active-border-color: var(--bs-success-text-emphasis); -} - -.list-group-item-info { - --bs-list-group-color: var(--bs-info-text-emphasis); - --bs-list-group-bg: var(--bs-info-bg-subtle); - --bs-list-group-border-color: var(--bs-info-border-subtle); - --bs-list-group-action-hover-color: var(--bs-emphasis-color); - --bs-list-group-action-hover-bg: var(--bs-info-border-subtle); - --bs-list-group-action-active-color: var(--bs-emphasis-color); - --bs-list-group-action-active-bg: var(--bs-info-border-subtle); - --bs-list-group-active-color: var(--bs-info-bg-subtle); - --bs-list-group-active-bg: var(--bs-info-text-emphasis); - --bs-list-group-active-border-color: var(--bs-info-text-emphasis); -} - -.list-group-item-warning { - --bs-list-group-color: var(--bs-warning-text-emphasis); - --bs-list-group-bg: var(--bs-warning-bg-subtle); - --bs-list-group-border-color: var(--bs-warning-border-subtle); - --bs-list-group-action-hover-color: var(--bs-emphasis-color); - --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle); - --bs-list-group-action-active-color: var(--bs-emphasis-color); - --bs-list-group-action-active-bg: var(--bs-warning-border-subtle); - --bs-list-group-active-color: var(--bs-warning-bg-subtle); - --bs-list-group-active-bg: var(--bs-warning-text-emphasis); - --bs-list-group-active-border-color: var(--bs-warning-text-emphasis); -} - -.list-group-item-danger { - --bs-list-group-color: var(--bs-danger-text-emphasis); - --bs-list-group-bg: var(--bs-danger-bg-subtle); - --bs-list-group-border-color: var(--bs-danger-border-subtle); - --bs-list-group-action-hover-color: var(--bs-emphasis-color); - --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle); - --bs-list-group-action-active-color: var(--bs-emphasis-color); - --bs-list-group-action-active-bg: var(--bs-danger-border-subtle); - --bs-list-group-active-color: var(--bs-danger-bg-subtle); - --bs-list-group-active-bg: var(--bs-danger-text-emphasis); - --bs-list-group-active-border-color: var(--bs-danger-text-emphasis); -} - -.list-group-item-light { - --bs-list-group-color: var(--bs-light-text-emphasis); - --bs-list-group-bg: var(--bs-light-bg-subtle); - --bs-list-group-border-color: var(--bs-light-border-subtle); - --bs-list-group-action-hover-color: var(--bs-emphasis-color); - --bs-list-group-action-hover-bg: var(--bs-light-border-subtle); - --bs-list-group-action-active-color: var(--bs-emphasis-color); - --bs-list-group-action-active-bg: var(--bs-light-border-subtle); - --bs-list-group-active-color: var(--bs-light-bg-subtle); - --bs-list-group-active-bg: var(--bs-light-text-emphasis); - --bs-list-group-active-border-color: var(--bs-light-text-emphasis); -} - -.list-group-item-dark { - --bs-list-group-color: var(--bs-dark-text-emphasis); - --bs-list-group-bg: var(--bs-dark-bg-subtle); - --bs-list-group-border-color: var(--bs-dark-border-subtle); - --bs-list-group-action-hover-color: var(--bs-emphasis-color); - --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle); - --bs-list-group-action-active-color: var(--bs-emphasis-color); - --bs-list-group-action-active-bg: var(--bs-dark-border-subtle); - --bs-list-group-active-color: var(--bs-dark-bg-subtle); - --bs-list-group-active-bg: var(--bs-dark-text-emphasis); - --bs-list-group-active-border-color: var(--bs-dark-text-emphasis); -} - -.btn-close { - --bs-btn-close-color: #fff; - --bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e"); - --bs-btn-close-opacity: 0.6; - --bs-btn-close-hover-opacity: 1; - --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(42, 159, 214, 0.25); - --bs-btn-close-focus-opacity: 1; - --bs-btn-close-disabled-opacity: 0.25; - --bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%); - box-sizing: content-box; - width: 1em; - height: 1em; - padding: 0.25em 0.25em; - color: var(--bs-btn-close-color); - background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat; - border: 0; - border-radius: 0.375rem; - opacity: var(--bs-btn-close-opacity); -} -.btn-close:hover { - color: var(--bs-btn-close-color); - text-decoration: none; - opacity: var(--bs-btn-close-hover-opacity); -} -.btn-close:focus { - outline: 0; - box-shadow: var(--bs-btn-close-focus-shadow); - opacity: var(--bs-btn-close-focus-opacity); -} -.btn-close:disabled, .btn-close.disabled { - pointer-events: none; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - opacity: var(--bs-btn-close-disabled-opacity); -} - -.btn-close-white { - filter: var(--bs-btn-close-white-filter); -} - -[data-bs-theme=dark] .btn-close { - filter: var(--bs-btn-close-white-filter); -} - -.toast { - --bs-toast-zindex: 1090; - --bs-toast-padding-x: 0.75rem; - --bs-toast-padding-y: 0.5rem; - --bs-toast-spacing: 1.5rem; - --bs-toast-max-width: 350px; - --bs-toast-font-size: 0.875rem; - --bs-toast-color: #fff; - --bs-toast-bg: #222; - --bs-toast-border-width: var(--bs-border-width); - --bs-toast-border-color: #282828; - --bs-toast-border-radius: var(--bs-border-radius); - --bs-toast-box-shadow: var(--bs-box-shadow); - --bs-toast-header-color: #adafae; - --bs-toast-header-bg: #222; - --bs-toast-header-border-color: #282828; - width: var(--bs-toast-max-width); - max-width: 100%; - font-size: var(--bs-toast-font-size); - color: var(--bs-toast-color); - pointer-events: auto; - background-color: var(--bs-toast-bg); - background-clip: padding-box; - border: var(--bs-toast-border-width) solid var(--bs-toast-border-color); - box-shadow: var(--bs-toast-box-shadow); - border-radius: var(--bs-toast-border-radius); -} -.toast.showing { - opacity: 0; -} -.toast:not(.show) { - display: none; -} - -.toast-container { - --bs-toast-zindex: 1090; - position: absolute; - z-index: var(--bs-toast-zindex); - width: -webkit-max-content; - width: -moz-max-content; - width: max-content; - max-width: 100%; - pointer-events: none; -} -.toast-container > :not(:last-child) { - margin-bottom: var(--bs-toast-spacing); -} - -.toast-header { - display: flex; - align-items: center; - padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x); - color: var(--bs-toast-header-color); - background-color: var(--bs-toast-header-bg); - background-clip: padding-box; - border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color); - border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); - border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); -} -.toast-header .btn-close { - margin-right: calc(-0.5 * var(--bs-toast-padding-x)); - margin-left: var(--bs-toast-padding-x); -} - -.toast-body { - padding: var(--bs-toast-padding-x); - word-wrap: break-word; -} - -.modal { - --bs-modal-zindex: 1055; - --bs-modal-width: 500px; - --bs-modal-padding: 1rem; - --bs-modal-margin: 0.5rem; - --bs-modal-color: ; - --bs-modal-bg: #222; - --bs-modal-border-color: var(--bs-border-color-translucent); - --bs-modal-border-width: var(--bs-border-width); - --bs-modal-border-radius: var(--bs-border-radius-lg); - --bs-modal-box-shadow: var(--bs-box-shadow-sm); - --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width))); - --bs-modal-header-padding-x: 1rem; - --bs-modal-header-padding-y: 1rem; - --bs-modal-header-padding: 1rem 1rem; - --bs-modal-header-border-color: #282828; - --bs-modal-header-border-width: var(--bs-border-width); - --bs-modal-title-line-height: 1.5; - --bs-modal-footer-gap: 0.5rem; - --bs-modal-footer-bg: ; - --bs-modal-footer-border-color: #282828; - --bs-modal-footer-border-width: var(--bs-border-width); - position: fixed; - top: 0; - left: 0; - z-index: var(--bs-modal-zindex); - display: none; - width: 100%; - height: 100%; - overflow-x: hidden; - overflow-y: auto; - outline: 0; -} - -.modal-dialog { - position: relative; - width: auto; - margin: var(--bs-modal-margin); - pointer-events: none; -} -.modal.fade .modal-dialog { - transition: transform 0.3s ease-out; - transform: translate(0, -50px); -} -@media (prefers-reduced-motion: reduce) { - .modal.fade .modal-dialog { - transition: none; - } -} -.modal.show .modal-dialog { - transform: none; -} -.modal.modal-static .modal-dialog { - transform: scale(1.02); -} - -.modal-dialog-scrollable { - height: calc(100% - var(--bs-modal-margin) * 2); -} -.modal-dialog-scrollable .modal-content { - max-height: 100%; - overflow: hidden; -} -.modal-dialog-scrollable .modal-body { - overflow-y: auto; -} - -.modal-dialog-centered { - display: flex; - align-items: center; - min-height: calc(100% - var(--bs-modal-margin) * 2); -} - -.modal-content { - position: relative; - display: flex; - flex-direction: column; - width: 100%; - color: var(--bs-modal-color); - pointer-events: auto; - background-color: var(--bs-modal-bg); - background-clip: padding-box; - border: var(--bs-modal-border-width) solid var(--bs-modal-border-color); - border-radius: var(--bs-modal-border-radius); - outline: 0; -} - -.modal-backdrop { - --bs-backdrop-zindex: 1050; - --bs-backdrop-bg: #000; - --bs-backdrop-opacity: 0.5; - position: fixed; - top: 0; - left: 0; - z-index: var(--bs-backdrop-zindex); - width: 100vw; - height: 100vh; - background-color: var(--bs-backdrop-bg); -} -.modal-backdrop.fade { - opacity: 0; -} -.modal-backdrop.show { - opacity: var(--bs-backdrop-opacity); -} - -.modal-header { - display: flex; - flex-shrink: 0; - align-items: center; - padding: var(--bs-modal-header-padding); - border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color); - border-top-left-radius: var(--bs-modal-inner-border-radius); - border-top-right-radius: var(--bs-modal-inner-border-radius); -} -.modal-header .btn-close { - padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5); - margin: calc(-0.5 * var(--bs-modal-header-padding-y)) calc(-0.5 * var(--bs-modal-header-padding-x)) calc(-0.5 * var(--bs-modal-header-padding-y)) auto; -} - -.modal-title { - margin-bottom: 0; - line-height: var(--bs-modal-title-line-height); -} - -.modal-body { - position: relative; - flex: 1 1 auto; - padding: var(--bs-modal-padding); -} - -.modal-footer { - display: flex; - flex-shrink: 0; - flex-wrap: wrap; - align-items: center; - justify-content: flex-end; - padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5); - background-color: var(--bs-modal-footer-bg); - border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color); - border-bottom-right-radius: var(--bs-modal-inner-border-radius); - border-bottom-left-radius: var(--bs-modal-inner-border-radius); -} -.modal-footer > * { - margin: calc(var(--bs-modal-footer-gap) * 0.5); -} - -@media (min-width: 576px) { - .modal { - --bs-modal-margin: 1.75rem; - --bs-modal-box-shadow: var(--bs-box-shadow); - } - .modal-dialog { - max-width: var(--bs-modal-width); - margin-right: auto; - margin-left: auto; - } - .modal-sm { - --bs-modal-width: 300px; - } -} -@media (min-width: 992px) { - .modal-lg, - .modal-xl { - --bs-modal-width: 800px; - } -} -@media (min-width: 1200px) { - .modal-xl { - --bs-modal-width: 1140px; - } -} -.modal-fullscreen { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; -} -.modal-fullscreen .modal-content { - height: 100%; - border: 0; - border-radius: 0; -} -.modal-fullscreen .modal-header, -.modal-fullscreen .modal-footer { - border-radius: 0; -} -.modal-fullscreen .modal-body { - overflow-y: auto; -} - -@media (max-width: 575.98px) { - .modal-fullscreen-sm-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-sm-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-sm-down .modal-header, - .modal-fullscreen-sm-down .modal-footer { - border-radius: 0; - } - .modal-fullscreen-sm-down .modal-body { - overflow-y: auto; - } -} -@media (max-width: 767.98px) { - .modal-fullscreen-md-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-md-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-md-down .modal-header, - .modal-fullscreen-md-down .modal-footer { - border-radius: 0; - } - .modal-fullscreen-md-down .modal-body { - overflow-y: auto; - } -} -@media (max-width: 991.98px) { - .modal-fullscreen-lg-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-lg-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-lg-down .modal-header, - .modal-fullscreen-lg-down .modal-footer { - border-radius: 0; - } - .modal-fullscreen-lg-down .modal-body { - overflow-y: auto; - } -} -@media (max-width: 1199.98px) { - .modal-fullscreen-xl-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-xl-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-xl-down .modal-header, - .modal-fullscreen-xl-down .modal-footer { - border-radius: 0; - } - .modal-fullscreen-xl-down .modal-body { - overflow-y: auto; - } -} -@media (max-width: 1399.98px) { - .modal-fullscreen-xxl-down { - width: 100vw; - max-width: none; - height: 100%; - margin: 0; - } - .modal-fullscreen-xxl-down .modal-content { - height: 100%; - border: 0; - border-radius: 0; - } - .modal-fullscreen-xxl-down .modal-header, - .modal-fullscreen-xxl-down .modal-footer { - border-radius: 0; - } - .modal-fullscreen-xxl-down .modal-body { - overflow-y: auto; - } -} -.tooltip { - --bs-tooltip-zindex: 1080; - --bs-tooltip-max-width: 200px; - --bs-tooltip-padding-x: 0.5rem; - --bs-tooltip-padding-y: 0.25rem; - --bs-tooltip-margin: ; - --bs-tooltip-font-size: 0.875rem; - --bs-tooltip-color: var(--bs-body-bg); - --bs-tooltip-bg: var(--bs-emphasis-color); - --bs-tooltip-border-radius: var(--bs-border-radius); - --bs-tooltip-opacity: 1; - --bs-tooltip-arrow-width: 0.8rem; - --bs-tooltip-arrow-height: 0.4rem; - z-index: var(--bs-tooltip-zindex); - display: block; - margin: var(--bs-tooltip-margin); - font-family: var(--bs-font-sans-serif); - font-style: normal; - font-weight: 400; - line-height: 1.5; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - letter-spacing: normal; - word-break: normal; - white-space: normal; - word-spacing: normal; - line-break: auto; - font-size: var(--bs-tooltip-font-size); - word-wrap: break-word; - opacity: 0; -} -.tooltip.show { - opacity: var(--bs-tooltip-opacity); -} -.tooltip .tooltip-arrow { - display: block; - width: var(--bs-tooltip-arrow-width); - height: var(--bs-tooltip-arrow-height); -} -.tooltip .tooltip-arrow::before { - position: absolute; - content: ""; - border-color: transparent; - border-style: solid; -} - -.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow { - bottom: calc(-1 * var(--bs-tooltip-arrow-height)); -} -.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before { - top: -1px; - border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; - border-top-color: var(--bs-tooltip-bg); -} - -/* rtl:begin:ignore */ -.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow { - left: calc(-1 * var(--bs-tooltip-arrow-height)); - width: var(--bs-tooltip-arrow-height); - height: var(--bs-tooltip-arrow-width); -} -.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before { - right: -1px; - border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; - border-right-color: var(--bs-tooltip-bg); -} - -/* rtl:end:ignore */ -.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow { - top: calc(-1 * var(--bs-tooltip-arrow-height)); -} -.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before { - bottom: -1px; - border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); - border-bottom-color: var(--bs-tooltip-bg); -} - -/* rtl:begin:ignore */ -.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow { - right: calc(-1 * var(--bs-tooltip-arrow-height)); - width: var(--bs-tooltip-arrow-height); - height: var(--bs-tooltip-arrow-width); -} -.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before { - left: -1px; - border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); - border-left-color: var(--bs-tooltip-bg); -} - -/* rtl:end:ignore */ -.tooltip-inner { - max-width: var(--bs-tooltip-max-width); - padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x); - color: var(--bs-tooltip-color); - text-align: center; - background-color: var(--bs-tooltip-bg); - border-radius: var(--bs-tooltip-border-radius); -} - -.popover { - --bs-popover-zindex: 1070; - --bs-popover-max-width: 276px; - --bs-popover-font-size: 0.875rem; - --bs-popover-bg: #282828; - --bs-popover-border-width: var(--bs-border-width); - --bs-popover-border-color: var(--bs-border-color-translucent); - --bs-popover-border-radius: var(--bs-border-radius-lg); - --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width)); - --bs-popover-box-shadow: var(--bs-box-shadow); - --bs-popover-header-padding-x: 1rem; - --bs-popover-header-padding-y: 0.5rem; - --bs-popover-header-font-size: 1rem; - --bs-popover-header-color: #fff; - --bs-popover-header-bg: var(--bs-secondary-bg); - --bs-popover-body-padding-x: 1rem; - --bs-popover-body-padding-y: 1rem; - --bs-popover-body-color: var(--bs-body-color); - --bs-popover-arrow-width: 1rem; - --bs-popover-arrow-height: 0.5rem; - --bs-popover-arrow-border: var(--bs-popover-border-color); - z-index: var(--bs-popover-zindex); - display: block; - max-width: var(--bs-popover-max-width); - font-family: var(--bs-font-sans-serif); - font-style: normal; - font-weight: 400; - line-height: 1.5; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - letter-spacing: normal; - word-break: normal; - white-space: normal; - word-spacing: normal; - line-break: auto; - font-size: var(--bs-popover-font-size); - word-wrap: break-word; - background-color: var(--bs-popover-bg); - background-clip: padding-box; - border: var(--bs-popover-border-width) solid var(--bs-popover-border-color); - border-radius: var(--bs-popover-border-radius); -} -.popover .popover-arrow { - display: block; - width: var(--bs-popover-arrow-width); - height: var(--bs-popover-arrow-height); -} -.popover .popover-arrow::before, .popover .popover-arrow::after { - position: absolute; - display: block; - content: ""; - border-color: transparent; - border-style: solid; - border-width: 0; -} - -.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow { - bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); -} -.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { - border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; -} -.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before { - bottom: 0; - border-top-color: var(--bs-popover-arrow-border); -} -.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { - bottom: var(--bs-popover-border-width); - border-top-color: var(--bs-popover-bg); -} - -/* rtl:begin:ignore */ -.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow { - left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); - width: var(--bs-popover-arrow-height); - height: var(--bs-popover-arrow-width); -} -.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before, .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { - border-width: calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; -} -.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before { - left: 0; - border-right-color: var(--bs-popover-arrow-border); -} -.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { - left: var(--bs-popover-border-width); - border-right-color: var(--bs-popover-bg); -} - -/* rtl:end:ignore */ -.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow { - top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); -} -.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { - border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); -} -.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before { - top: 0; - border-bottom-color: var(--bs-popover-arrow-border); -} -.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { - top: var(--bs-popover-border-width); - border-bottom-color: var(--bs-popover-bg); -} -.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before { - position: absolute; - top: 0; - left: 50%; - display: block; - width: var(--bs-popover-arrow-width); - margin-left: calc(-0.5 * var(--bs-popover-arrow-width)); - content: ""; - border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg); -} - -/* rtl:begin:ignore */ -.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow { - right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); - width: var(--bs-popover-arrow-height); - height: var(--bs-popover-arrow-width); -} -.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before, .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { - border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); -} -.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before { - right: 0; - border-left-color: var(--bs-popover-arrow-border); -} -.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { - right: var(--bs-popover-border-width); - border-left-color: var(--bs-popover-bg); -} - -/* rtl:end:ignore */ -.popover-header { - padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x); - margin-bottom: 0; - font-size: var(--bs-popover-header-font-size); - color: var(--bs-popover-header-color); - background-color: var(--bs-popover-header-bg); - border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color); - border-top-left-radius: var(--bs-popover-inner-border-radius); - border-top-right-radius: var(--bs-popover-inner-border-radius); -} -.popover-header:empty { - display: none; -} - -.popover-body { - padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x); - color: var(--bs-popover-body-color); -} - -.carousel { - position: relative; -} - -.carousel.pointer-event { - touch-action: pan-y; -} - -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; -} -.carousel-inner::after { - display: block; - clear: both; - content: ""; -} - -.carousel-item { - position: relative; - display: none; - float: left; - width: 100%; - margin-right: -100%; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - transition: transform 0.6s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .carousel-item { - transition: none; - } -} - -.carousel-item.active, -.carousel-item-next, -.carousel-item-prev { - display: block; -} - -.carousel-item-next:not(.carousel-item-start), -.active.carousel-item-end { - transform: translateX(100%); -} - -.carousel-item-prev:not(.carousel-item-end), -.active.carousel-item-start { - transform: translateX(-100%); -} - -.carousel-fade .carousel-item { - opacity: 0; - transition-property: opacity; - transform: none; -} -.carousel-fade .carousel-item.active, -.carousel-fade .carousel-item-next.carousel-item-start, -.carousel-fade .carousel-item-prev.carousel-item-end { - z-index: 1; - opacity: 1; -} -.carousel-fade .active.carousel-item-start, -.carousel-fade .active.carousel-item-end { - z-index: 0; - opacity: 0; - transition: opacity 0s 0.6s; -} -@media (prefers-reduced-motion: reduce) { - .carousel-fade .active.carousel-item-start, - .carousel-fade .active.carousel-item-end { - transition: none; - } -} - -.carousel-control-prev, -.carousel-control-next { - position: absolute; - top: 0; - bottom: 0; - z-index: 1; - display: flex; - align-items: center; - justify-content: center; - width: 15%; - padding: 0; - color: #fff; - text-align: center; - background: none; - border: 0; - opacity: 0.5; - transition: opacity 0.15s ease; -} -@media (prefers-reduced-motion: reduce) { - .carousel-control-prev, - .carousel-control-next { - transition: none; - } -} -.carousel-control-prev:hover, .carousel-control-prev:focus, -.carousel-control-next:hover, -.carousel-control-next:focus { - color: #fff; - text-decoration: none; - outline: 0; - opacity: 0.9; -} - -.carousel-control-prev { - left: 0; -} - -.carousel-control-next { - right: 0; -} - -.carousel-control-prev-icon, -.carousel-control-next-icon { - display: inline-block; - width: 2rem; - height: 2rem; - background-repeat: no-repeat; - background-position: 50%; - background-size: 100% 100%; -} - -.carousel-control-prev-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")*/; -} - -.carousel-control-next-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")*/; -} - -.carousel-indicators { - position: absolute; - right: 0; - bottom: 0; - left: 0; - z-index: 2; - display: flex; - justify-content: center; - padding: 0; - margin-right: 15%; - margin-bottom: 1rem; - margin-left: 15%; -} -.carousel-indicators [data-bs-target] { - box-sizing: content-box; - flex: 0 1 auto; - width: 30px; - height: 3px; - padding: 0; - margin-right: 3px; - margin-left: 3px; - text-indent: -999px; - cursor: pointer; - background-color: #fff; - background-clip: padding-box; - border: 0; - border-top: 10px solid transparent; - border-bottom: 10px solid transparent; - opacity: 0.5; - transition: opacity 0.6s ease; -} -@media (prefers-reduced-motion: reduce) { - .carousel-indicators [data-bs-target] { - transition: none; - } -} -.carousel-indicators .active { - opacity: 1; -} - -.carousel-caption { - position: absolute; - right: 15%; - bottom: 1.25rem; - left: 15%; - padding-top: 1.25rem; - padding-bottom: 1.25rem; - color: #fff; - text-align: center; -} - -.carousel-dark .carousel-control-prev-icon, -.carousel-dark .carousel-control-next-icon { - filter: invert(1) grayscale(100); -} -.carousel-dark .carousel-indicators [data-bs-target] { - background-color: #000; -} -.carousel-dark .carousel-caption { - color: #000; -} - -[data-bs-theme=dark] .carousel .carousel-control-prev-icon, -[data-bs-theme=dark] .carousel .carousel-control-next-icon, [data-bs-theme=dark].carousel .carousel-control-prev-icon, -[data-bs-theme=dark].carousel .carousel-control-next-icon { - filter: invert(1) grayscale(100); -} -[data-bs-theme=dark] .carousel .carousel-indicators [data-bs-target], [data-bs-theme=dark].carousel .carousel-indicators [data-bs-target] { - background-color: #000; -} -[data-bs-theme=dark] .carousel .carousel-caption, [data-bs-theme=dark].carousel .carousel-caption { - color: #000; -} - -.spinner-grow, -.spinner-border { - display: inline-block; - width: var(--bs-spinner-width); - height: var(--bs-spinner-height); - vertical-align: var(--bs-spinner-vertical-align); - border-radius: 50%; - animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name); -} - -@keyframes spinner-border { - to { - transform: rotate(360deg) /* rtl:ignore */; - } -} -.spinner-border { - --bs-spinner-width: 2rem; - --bs-spinner-height: 2rem; - --bs-spinner-vertical-align: -0.125em; - --bs-spinner-border-width: 0.25em; - --bs-spinner-animation-speed: 0.75s; - --bs-spinner-animation-name: spinner-border; - border: var(--bs-spinner-border-width) solid currentcolor; - border-right-color: transparent; -} - -.spinner-border-sm { - --bs-spinner-width: 1rem; - --bs-spinner-height: 1rem; - --bs-spinner-border-width: 0.2em; -} - -@keyframes spinner-grow { - 0% { - transform: scale(0); - } - 50% { - opacity: 1; - transform: none; - } -} -.spinner-grow { - --bs-spinner-width: 2rem; - --bs-spinner-height: 2rem; - --bs-spinner-vertical-align: -0.125em; - --bs-spinner-animation-speed: 0.75s; - --bs-spinner-animation-name: spinner-grow; - background-color: currentcolor; - opacity: 0; -} - -.spinner-grow-sm { - --bs-spinner-width: 1rem; - --bs-spinner-height: 1rem; -} - -@media (prefers-reduced-motion: reduce) { - .spinner-border, - .spinner-grow { - --bs-spinner-animation-speed: 1.5s; - } -} -.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm { - --bs-offcanvas-zindex: 1045; - --bs-offcanvas-width: 400px; - --bs-offcanvas-height: 30vh; - --bs-offcanvas-padding-x: 1rem; - --bs-offcanvas-padding-y: 1rem; - --bs-offcanvas-color: var(--bs-body-color); - --bs-offcanvas-bg: var(--bs-body-bg); - --bs-offcanvas-border-width: var(--bs-border-width); - --bs-offcanvas-border-color: var(--bs-border-color-translucent); - --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm); - --bs-offcanvas-transition: transform 0.3s ease-in-out; - --bs-offcanvas-title-line-height: 1.5; -} - -@media (max-width: 575.98px) { - .offcanvas-sm { - position: fixed; - bottom: 0; - z-index: var(--bs-offcanvas-zindex); - display: flex; - flex-direction: column; - max-width: 100%; - color: var(--bs-offcanvas-color); - visibility: hidden; - background-color: var(--bs-offcanvas-bg); - background-clip: padding-box; - outline: 0; - transition: var(--bs-offcanvas-transition); - } -} -@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) { - .offcanvas-sm { - transition: none; - } -} -@media (max-width: 575.98px) { - .offcanvas-sm.offcanvas-start { - top: 0; - left: 0; - width: var(--bs-offcanvas-width); - border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(-100%); - } - .offcanvas-sm.offcanvas-end { - top: 0; - right: 0; - width: var(--bs-offcanvas-width); - border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(100%); - } - .offcanvas-sm.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(-100%); - } - .offcanvas-sm.offcanvas-bottom { - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(100%); - } - .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) { - transform: none; - } - .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show { - visibility: visible; - } -} -@media (min-width: 576px) { - .offcanvas-sm { - --bs-offcanvas-height: auto; - --bs-offcanvas-border-width: 0; - background-color: transparent !important; - } - .offcanvas-sm .offcanvas-header { - display: none; - } - .offcanvas-sm .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - background-color: transparent !important; - } -} - -@media (max-width: 767.98px) { - .offcanvas-md { - position: fixed; - bottom: 0; - z-index: var(--bs-offcanvas-zindex); - display: flex; - flex-direction: column; - max-width: 100%; - color: var(--bs-offcanvas-color); - visibility: hidden; - background-color: var(--bs-offcanvas-bg); - background-clip: padding-box; - outline: 0; - transition: var(--bs-offcanvas-transition); - } -} -@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) { - .offcanvas-md { - transition: none; - } -} -@media (max-width: 767.98px) { - .offcanvas-md.offcanvas-start { - top: 0; - left: 0; - width: var(--bs-offcanvas-width); - border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(-100%); - } - .offcanvas-md.offcanvas-end { - top: 0; - right: 0; - width: var(--bs-offcanvas-width); - border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(100%); - } - .offcanvas-md.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(-100%); - } - .offcanvas-md.offcanvas-bottom { - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(100%); - } - .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) { - transform: none; - } - .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show { - visibility: visible; - } -} -@media (min-width: 768px) { - .offcanvas-md { - --bs-offcanvas-height: auto; - --bs-offcanvas-border-width: 0; - background-color: transparent !important; - } - .offcanvas-md .offcanvas-header { - display: none; - } - .offcanvas-md .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - background-color: transparent !important; - } -} - -@media (max-width: 991.98px) { - .offcanvas-lg { - position: fixed; - bottom: 0; - z-index: var(--bs-offcanvas-zindex); - display: flex; - flex-direction: column; - max-width: 100%; - color: var(--bs-offcanvas-color); - visibility: hidden; - background-color: var(--bs-offcanvas-bg); - background-clip: padding-box; - outline: 0; - transition: var(--bs-offcanvas-transition); - } -} -@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) { - .offcanvas-lg { - transition: none; - } -} -@media (max-width: 991.98px) { - .offcanvas-lg.offcanvas-start { - top: 0; - left: 0; - width: var(--bs-offcanvas-width); - border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(-100%); - } - .offcanvas-lg.offcanvas-end { - top: 0; - right: 0; - width: var(--bs-offcanvas-width); - border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(100%); - } - .offcanvas-lg.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(-100%); - } - .offcanvas-lg.offcanvas-bottom { - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(100%); - } - .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) { - transform: none; - } - .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show { - visibility: visible; - } -} -@media (min-width: 992px) { - .offcanvas-lg { - --bs-offcanvas-height: auto; - --bs-offcanvas-border-width: 0; - background-color: transparent !important; - } - .offcanvas-lg .offcanvas-header { - display: none; - } - .offcanvas-lg .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - background-color: transparent !important; - } -} - -@media (max-width: 1199.98px) { - .offcanvas-xl { - position: fixed; - bottom: 0; - z-index: var(--bs-offcanvas-zindex); - display: flex; - flex-direction: column; - max-width: 100%; - color: var(--bs-offcanvas-color); - visibility: hidden; - background-color: var(--bs-offcanvas-bg); - background-clip: padding-box; - outline: 0; - transition: var(--bs-offcanvas-transition); - } -} -@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) { - .offcanvas-xl { - transition: none; - } -} -@media (max-width: 1199.98px) { - .offcanvas-xl.offcanvas-start { - top: 0; - left: 0; - width: var(--bs-offcanvas-width); - border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(-100%); - } - .offcanvas-xl.offcanvas-end { - top: 0; - right: 0; - width: var(--bs-offcanvas-width); - border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(100%); - } - .offcanvas-xl.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(-100%); - } - .offcanvas-xl.offcanvas-bottom { - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(100%); - } - .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) { - transform: none; - } - .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show { - visibility: visible; - } -} -@media (min-width: 1200px) { - .offcanvas-xl { - --bs-offcanvas-height: auto; - --bs-offcanvas-border-width: 0; - background-color: transparent !important; - } - .offcanvas-xl .offcanvas-header { - display: none; - } - .offcanvas-xl .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - background-color: transparent !important; - } -} - -@media (max-width: 1399.98px) { - .offcanvas-xxl { - position: fixed; - bottom: 0; - z-index: var(--bs-offcanvas-zindex); - display: flex; - flex-direction: column; - max-width: 100%; - color: var(--bs-offcanvas-color); - visibility: hidden; - background-color: var(--bs-offcanvas-bg); - background-clip: padding-box; - outline: 0; - transition: var(--bs-offcanvas-transition); - } -} -@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) { - .offcanvas-xxl { - transition: none; - } -} -@media (max-width: 1399.98px) { - .offcanvas-xxl.offcanvas-start { - top: 0; - left: 0; - width: var(--bs-offcanvas-width); - border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(-100%); - } - .offcanvas-xxl.offcanvas-end { - top: 0; - right: 0; - width: var(--bs-offcanvas-width); - border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(100%); - } - .offcanvas-xxl.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(-100%); - } - .offcanvas-xxl.offcanvas-bottom { - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(100%); - } - .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) { - transform: none; - } - .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show { - visibility: visible; - } -} -@media (min-width: 1400px) { - .offcanvas-xxl { - --bs-offcanvas-height: auto; - --bs-offcanvas-border-width: 0; - background-color: transparent !important; - } - .offcanvas-xxl .offcanvas-header { - display: none; - } - .offcanvas-xxl .offcanvas-body { - display: flex; - flex-grow: 0; - padding: 0; - overflow-y: visible; - background-color: transparent !important; - } -} - -.offcanvas { - position: fixed; - bottom: 0; - z-index: var(--bs-offcanvas-zindex); - display: flex; - flex-direction: column; - max-width: 100%; - color: var(--bs-offcanvas-color); - visibility: hidden; - background-color: var(--bs-offcanvas-bg); - background-clip: padding-box; - outline: 0; - transition: var(--bs-offcanvas-transition); -} -@media (prefers-reduced-motion: reduce) { - .offcanvas { - transition: none; - } -} -.offcanvas.offcanvas-start { - top: 0; - left: 0; - width: var(--bs-offcanvas-width); - border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(-100%); -} -.offcanvas.offcanvas-end { - top: 0; - right: 0; - width: var(--bs-offcanvas-width); - border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateX(100%); -} -.offcanvas.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(-100%); -} -.offcanvas.offcanvas-bottom { - right: 0; - left: 0; - height: var(--bs-offcanvas-height); - max-height: 100%; - border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); - transform: translateY(100%); -} -.offcanvas.showing, .offcanvas.show:not(.hiding) { - transform: none; -} -.offcanvas.showing, .offcanvas.hiding, .offcanvas.show { - visibility: visible; -} - -.offcanvas-backdrop { - position: fixed; - top: 0; - left: 0; - z-index: 1040; - width: 100vw; - height: 100vh; - background-color: #000; -} -.offcanvas-backdrop.fade { - opacity: 0; -} -.offcanvas-backdrop.show { - opacity: 0.5; -} - -.offcanvas-header { - display: flex; - align-items: center; - padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); -} -.offcanvas-header .btn-close { - padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5); - margin: calc(-0.5 * var(--bs-offcanvas-padding-y)) calc(-0.5 * var(--bs-offcanvas-padding-x)) calc(-0.5 * var(--bs-offcanvas-padding-y)) auto; -} - -.offcanvas-title { - margin-bottom: 0; - line-height: var(--bs-offcanvas-title-line-height); -} - -.offcanvas-body { - flex-grow: 1; - padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); - overflow-y: auto; -} - -.placeholder { - display: inline-block; - min-height: 1em; - vertical-align: middle; - cursor: wait; - background-color: currentcolor; - opacity: 0.5; -} -.placeholder.btn::before { - display: inline-block; - content: ""; -} - -.placeholder-xs { - min-height: 0.6em; -} - -.placeholder-sm { - min-height: 0.8em; -} - -.placeholder-lg { - min-height: 1.2em; -} - -.placeholder-glow .placeholder { - animation: placeholder-glow 2s ease-in-out infinite; -} - -@keyframes placeholder-glow { - 50% { - opacity: 0.2; - } -} -.placeholder-wave { - -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); - mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); - -webkit-mask-size: 200% 100%; - mask-size: 200% 100%; - animation: placeholder-wave 2s linear infinite; -} - -@keyframes placeholder-wave { - 100% { - -webkit-mask-position: -200% 0%; - mask-position: -200% 0%; - } -} -.clearfix::after { - display: block; - clear: both; - content: ""; -} - -.text-bg-primary { - color: #fff !important; - background-color: RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important; -} - -.text-bg-secondary { - color: #fff !important; - background-color: RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important; -} - -.text-bg-success { - color: #fff !important; - background-color: RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important; -} - -.text-bg-info { - color: #fff !important; - background-color: RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important; -} - -.text-bg-warning { - color: #fff !important; - background-color: RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important; -} - -.text-bg-danger { - color: #fff !important; - background-color: RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important; -} - -.text-bg-light { - color: #fff !important; - background-color: RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important; -} - -.text-bg-dark { - color: #000 !important; - background-color: RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important; -} - -.link-primary { - color: RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; -} -.link-primary:hover, .link-primary:focus { - color: RGBA(34, 127, 171, var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(34, 127, 171, var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(34, 127, 171, var(--bs-link-underline-opacity, 1)) !important; -} - -.link-secondary { - color: RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; -} -.link-secondary:hover, .link-secondary:focus { - color: RGBA(68, 68, 68, var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(68, 68, 68, var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(68, 68, 68, var(--bs-link-underline-opacity, 1)) !important; -} - -.link-success { - color: RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; -} -.link-success:hover, .link-success:focus { - color: RGBA(95, 143, 0, var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(95, 143, 0, var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(95, 143, 0, var(--bs-link-underline-opacity, 1)) !important; -} - -.link-info { - color: RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; -} -.link-info:hover, .link-info:focus { - color: RGBA(122, 41, 163, var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(122, 41, 163, var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(122, 41, 163, var(--bs-link-underline-opacity, 1)) !important; -} - -.link-warning { - color: RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; -} -.link-warning:hover, .link-warning:focus { - color: RGBA(204, 109, 0, var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(204, 109, 0, var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(204, 109, 0, var(--bs-link-underline-opacity, 1)) !important; -} - -.link-danger { - color: RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; -} -.link-danger:hover, .link-danger:focus { - color: RGBA(163, 0, 0, var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(163, 0, 0, var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(163, 0, 0, var(--bs-link-underline-opacity, 1)) !important; -} - -.link-light { - color: RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; -} -.link-light:hover, .link-light:focus { - color: RGBA(27, 27, 27, var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(27, 27, 27, var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(27, 27, 27, var(--bs-link-underline-opacity, 1)) !important; -} - -.link-dark { - color: RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; -} -.link-dark:hover, .link-dark:focus { - color: RGBA(189, 191, 190, var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(189, 191, 190, var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(189, 191, 190, var(--bs-link-underline-opacity, 1)) !important; -} - -.link-body-emphasis { - color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; -} -.link-body-emphasis:hover, .link-body-emphasis:focus { - color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important; - -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; - text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; -} - -.focus-ring:focus { - outline: 0; - box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color); -} - -.icon-link { - display: inline-flex; - gap: 0.375rem; - align-items: center; - -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5)); - text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5)); - text-underline-offset: 0.25em; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; -} -.icon-link > .bi { - flex-shrink: 0; - width: 1em; - height: 1em; - fill: currentcolor; - transition: 0.2s ease-in-out transform; -} -@media (prefers-reduced-motion: reduce) { - .icon-link > .bi { - transition: none; - } -} - -.icon-link-hover:hover > .bi, .icon-link-hover:focus-visible > .bi { - transform: var(--bs-icon-link-transform, translate3d(0.25em, 0, 0)); -} - -.ratio { - position: relative; - width: 100%; -} -.ratio::before { - display: block; - padding-top: var(--bs-aspect-ratio); - content: ""; -} -.ratio > * { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -.ratio-1x1 { - --bs-aspect-ratio: 100%; -} - -.ratio-4x3 { - --bs-aspect-ratio: 75%; -} - -.ratio-16x9 { - --bs-aspect-ratio: 56.25%; -} - -.ratio-21x9 { - --bs-aspect-ratio: 42.8571428571%; -} - -.fixed-top { - position: fixed; - top: 0; - right: 0; - left: 0; - z-index: 1030; -} - -.fixed-bottom { - position: fixed; - right: 0; - bottom: 0; - left: 0; - z-index: 1030; -} - -.sticky-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; -} - -.sticky-bottom { - position: -webkit-sticky; - position: sticky; - bottom: 0; - z-index: 1020; -} - -@media (min-width: 576px) { - .sticky-sm-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } - .sticky-sm-bottom { - position: -webkit-sticky; - position: sticky; - bottom: 0; - z-index: 1020; - } -} -@media (min-width: 768px) { - .sticky-md-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } - .sticky-md-bottom { - position: -webkit-sticky; - position: sticky; - bottom: 0; - z-index: 1020; - } -} -@media (min-width: 992px) { - .sticky-lg-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } - .sticky-lg-bottom { - position: -webkit-sticky; - position: sticky; - bottom: 0; - z-index: 1020; - } -} -@media (min-width: 1200px) { - .sticky-xl-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } - .sticky-xl-bottom { - position: -webkit-sticky; - position: sticky; - bottom: 0; - z-index: 1020; - } -} -@media (min-width: 1400px) { - .sticky-xxl-top { - position: -webkit-sticky; - position: sticky; - top: 0; - z-index: 1020; - } - .sticky-xxl-bottom { - position: -webkit-sticky; - position: sticky; - bottom: 0; - z-index: 1020; - } -} -.hstack { - display: flex; - flex-direction: row; - align-items: center; - align-self: stretch; -} - -.vstack { - display: flex; - flex: 1 1 auto; - flex-direction: column; - align-self: stretch; -} - -.visually-hidden, -.visually-hidden-focusable:not(:focus):not(:focus-within) { - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; - clip: rect(0, 0, 0, 0) !important; - white-space: nowrap !important; - border: 0 !important; -} -.visually-hidden:not(caption), -.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) { - position: absolute !important; -} - -.stretched-link::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1; - content: ""; -} - -.text-truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.vr { - display: inline-block; - align-self: stretch; - width: var(--bs-border-width); - min-height: 1em; - background-color: currentcolor; - opacity: 0.25; -} - -.align-baseline { - vertical-align: baseline !important; -} - -.align-top { - vertical-align: top !important; -} - -.align-middle { - vertical-align: middle !important; -} - -.align-bottom { - vertical-align: bottom !important; -} - -.align-text-bottom { - vertical-align: text-bottom !important; -} - -.align-text-top { - vertical-align: text-top !important; -} - -.float-start { - float: left !important; -} - -.float-end { - float: right !important; -} - -.float-none { - float: none !important; -} - -.object-fit-contain { - -o-object-fit: contain !important; - object-fit: contain !important; -} - -.object-fit-cover { - -o-object-fit: cover !important; - object-fit: cover !important; -} - -.object-fit-fill { - -o-object-fit: fill !important; - object-fit: fill !important; -} - -.object-fit-scale { - -o-object-fit: scale-down !important; - object-fit: scale-down !important; -} - -.object-fit-none { - -o-object-fit: none !important; - object-fit: none !important; -} - -.opacity-0 { - opacity: 0 !important; -} - -.opacity-25 { - opacity: 0.25 !important; -} - -.opacity-50 { - opacity: 0.5 !important; -} - -.opacity-75 { - opacity: 0.75 !important; -} - -.opacity-100 { - opacity: 1 !important; -} - -.overflow-auto { - overflow: auto !important; -} - -.overflow-hidden { - overflow: hidden !important; -} - -.overflow-visible { - overflow: visible !important; -} - -.overflow-scroll { - overflow: scroll !important; -} - -.overflow-x-auto { - overflow-x: auto !important; -} - -.overflow-x-hidden { - overflow-x: hidden !important; -} - -.overflow-x-visible { - overflow-x: visible !important; -} - -.overflow-x-scroll { - overflow-x: scroll !important; -} - -.overflow-y-auto { - overflow-y: auto !important; -} - -.overflow-y-hidden { - overflow-y: hidden !important; -} - -.overflow-y-visible { - overflow-y: visible !important; -} - -.overflow-y-scroll { - overflow-y: scroll !important; -} - -.d-inline { - display: inline !important; -} - -.d-inline-block { - display: inline-block !important; -} - -.d-block { - display: block !important; -} - -.d-grid { - display: grid !important; -} - -.d-inline-grid { - display: inline-grid !important; -} - -.d-table { - display: table !important; -} - -.d-table-row { - display: table-row !important; -} - -.d-table-cell { - display: table-cell !important; -} - -.d-flex { - display: flex !important; -} - -.d-inline-flex { - display: inline-flex !important; -} - -.d-none { - display: none !important; -} - -.shadow { - box-shadow: var(--bs-box-shadow) !important; -} - -.shadow-sm { - box-shadow: var(--bs-box-shadow-sm) !important; -} - -.shadow-lg { - box-shadow: var(--bs-box-shadow-lg) !important; -} - -.shadow-none { - box-shadow: none !important; -} - -.focus-ring-primary { - --bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity)); -} - -.focus-ring-secondary { - --bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity)); -} - -.focus-ring-success { - --bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity)); -} - -.focus-ring-info { - --bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity)); -} - -.focus-ring-warning { - --bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity)); -} - -.focus-ring-danger { - --bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity)); -} - -.focus-ring-light { - --bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity)); -} - -.focus-ring-dark { - --bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity)); -} - -.position-static { - position: static !important; -} - -.position-relative { - position: relative !important; -} - -.position-absolute { - position: absolute !important; -} - -.position-fixed { - position: fixed !important; -} - -.position-sticky { - position: -webkit-sticky !important; - position: sticky !important; -} - -.top-0 { - top: 0 !important; -} - -.top-50 { - top: 50% !important; -} - -.top-100 { - top: 100% !important; -} - -.bottom-0 { - bottom: 0 !important; -} - -.bottom-50 { - bottom: 50% !important; -} - -.bottom-100 { - bottom: 100% !important; -} - -.start-0 { - left: 0 !important; -} - -.start-50 { - left: 50% !important; -} - -.start-100 { - left: 100% !important; -} - -.end-0 { - right: 0 !important; -} - -.end-50 { - right: 50% !important; -} - -.end-100 { - right: 100% !important; -} - -.translate-middle { - transform: translate(-50%, -50%) !important; -} - -.translate-middle-x { - transform: translateX(-50%) !important; -} - -.translate-middle-y { - transform: translateY(-50%) !important; -} - -.border { - border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; -} - -.border-0 { - border: 0 !important; -} - -.border-top { - border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; -} - -.border-top-0 { - border-top: 0 !important; -} - -.border-end { - border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; -} - -.border-end-0 { - border-right: 0 !important; -} - -.border-bottom { - border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; -} - -.border-bottom-0 { - border-bottom: 0 !important; -} - -.border-start { - border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; -} - -.border-start-0 { - border-left: 0 !important; -} - -.border-primary { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important; -} - -.border-secondary { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important; -} - -.border-success { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important; -} - -.border-info { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important; -} - -.border-warning { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important; -} - -.border-danger { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important; -} - -.border-light { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important; -} - -.border-dark { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important; -} - -.border-black { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important; -} - -.border-white { - --bs-border-opacity: 1; - border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important; -} - -.border-primary-subtle { - border-color: var(--bs-primary-border-subtle) !important; -} - -.border-secondary-subtle { - border-color: var(--bs-secondary-border-subtle) !important; -} - -.border-success-subtle { - border-color: var(--bs-success-border-subtle) !important; -} - -.border-info-subtle { - border-color: var(--bs-info-border-subtle) !important; -} - -.border-warning-subtle { - border-color: var(--bs-warning-border-subtle) !important; -} - -.border-danger-subtle { - border-color: var(--bs-danger-border-subtle) !important; -} - -.border-light-subtle { - border-color: var(--bs-light-border-subtle) !important; -} - -.border-dark-subtle { - border-color: var(--bs-dark-border-subtle) !important; -} - -.border-1 { - border-width: 1px !important; -} - -.border-2 { - border-width: 2px !important; -} - -.border-3 { - border-width: 3px !important; -} - -.border-4 { - border-width: 4px !important; -} - -.border-5 { - border-width: 5px !important; -} - -.border-opacity-10 { - --bs-border-opacity: 0.1; -} - -.border-opacity-25 { - --bs-border-opacity: 0.25; -} - -.border-opacity-50 { - --bs-border-opacity: 0.5; -} - -.border-opacity-75 { - --bs-border-opacity: 0.75; -} - -.border-opacity-100 { - --bs-border-opacity: 1; -} - -.w-25 { - width: 25% !important; -} - -.w-50 { - width: 50% !important; -} - -.w-75 { - width: 75% !important; -} - -.w-100 { - width: 100% !important; -} - -.w-auto { - width: auto !important; -} - -.mw-100 { - max-width: 100% !important; -} - -.vw-100 { - width: 100vw !important; -} - -.min-vw-100 { - min-width: 100vw !important; -} - -.h-25 { - height: 25% !important; -} - -.h-50 { - height: 50% !important; -} - -.h-75 { - height: 75% !important; -} - -.h-100 { - height: 100% !important; -} - -.h-auto { - height: auto !important; -} - -.mh-100 { - max-height: 100% !important; -} - -.vh-100 { - height: 100vh !important; -} - -.min-vh-100 { - min-height: 100vh !important; -} - -.flex-fill { - flex: 1 1 auto !important; -} - -.flex-row { - flex-direction: row !important; -} - -.flex-column { - flex-direction: column !important; -} - -.flex-row-reverse { - flex-direction: row-reverse !important; -} - -.flex-column-reverse { - flex-direction: column-reverse !important; -} - -.flex-grow-0 { - flex-grow: 0 !important; -} - -.flex-grow-1 { - flex-grow: 1 !important; -} - -.flex-shrink-0 { - flex-shrink: 0 !important; -} - -.flex-shrink-1 { - flex-shrink: 1 !important; -} - -.flex-wrap { - flex-wrap: wrap !important; -} - -.flex-nowrap { - flex-wrap: nowrap !important; -} - -.flex-wrap-reverse { - flex-wrap: wrap-reverse !important; -} - -.justify-content-start { - justify-content: flex-start !important; -} - -.justify-content-end { - justify-content: flex-end !important; -} - -.justify-content-center { - justify-content: center !important; -} - -.justify-content-between { - justify-content: space-between !important; -} - -.justify-content-around { - justify-content: space-around !important; -} - -.justify-content-evenly { - justify-content: space-evenly !important; -} - -.align-items-start { - align-items: flex-start !important; -} - -.align-items-end { - align-items: flex-end !important; -} - -.align-items-center { - align-items: center !important; -} - -.align-items-baseline { - align-items: baseline !important; -} - -.align-items-stretch { - align-items: stretch !important; -} - -.align-content-start { - align-content: flex-start !important; -} - -.align-content-end { - align-content: flex-end !important; -} - -.align-content-center { - align-content: center !important; -} - -.align-content-between { - align-content: space-between !important; -} - -.align-content-around { - align-content: space-around !important; -} - -.align-content-stretch { - align-content: stretch !important; -} - -.align-self-auto { - align-self: auto !important; -} - -.align-self-start { - align-self: flex-start !important; -} - -.align-self-end { - align-self: flex-end !important; -} - -.align-self-center { - align-self: center !important; -} - -.align-self-baseline { - align-self: baseline !important; -} - -.align-self-stretch { - align-self: stretch !important; -} - -.order-first { - order: -1 !important; -} - -.order-0 { - order: 0 !important; -} - -.order-1 { - order: 1 !important; -} - -.order-2 { - order: 2 !important; -} - -.order-3 { - order: 3 !important; -} - -.order-4 { - order: 4 !important; -} - -.order-5 { - order: 5 !important; -} - -.order-last { - order: 6 !important; -} - -.m-0 { - margin: 0 !important; -} - -.m-1 { - margin: 0.25rem !important; -} - -.m-2 { - margin: 0.5rem !important; -} - -.m-3 { - margin: 1rem !important; -} - -.m-4 { - margin: 1.5rem !important; -} - -.m-5 { - margin: 3rem !important; -} - -.m-auto { - margin: auto !important; -} - -.mx-0 { - margin-right: 0 !important; - margin-left: 0 !important; -} - -.mx-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; -} - -.mx-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; -} - -.mx-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; -} - -.mx-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; -} - -.mx-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; -} - -.mx-auto { - margin-right: auto !important; - margin-left: auto !important; -} - -.my-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.my-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; -} - -.my-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; -} - -.my-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; -} - -.my-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; -} - -.my-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; -} - -.my-auto { - margin-top: auto !important; - margin-bottom: auto !important; -} - -.mt-0 { - margin-top: 0 !important; -} - -.mt-1 { - margin-top: 0.25rem !important; -} - -.mt-2 { - margin-top: 0.5rem !important; -} - -.mt-3 { - margin-top: 1rem !important; -} - -.mt-4 { - margin-top: 1.5rem !important; -} - -.mt-5 { - margin-top: 3rem !important; -} - -.mt-auto { - margin-top: auto !important; -} - -.me-0 { - margin-right: 0 !important; -} - -.me-1 { - margin-right: 0.25rem !important; -} - -.me-2 { - margin-right: 0.5rem !important; -} - -.me-3 { - margin-right: 1rem !important; -} - -.me-4 { - margin-right: 1.5rem !important; -} - -.me-5 { - margin-right: 3rem !important; -} - -.me-auto { - margin-right: auto !important; -} - -.mb-0 { - margin-bottom: 0 !important; -} - -.mb-1 { - margin-bottom: 0.25rem !important; -} - -.mb-2 { - margin-bottom: 0.5rem !important; -} - -.mb-3 { - margin-bottom: 1rem !important; -} - -.mb-4 { - margin-bottom: 1.5rem !important; -} - -.mb-5 { - margin-bottom: 3rem !important; -} - -.mb-auto { - margin-bottom: auto !important; -} - -.ms-0 { - margin-left: 0 !important; -} - -.ms-1 { - margin-left: 0.25rem !important; -} - -.ms-2 { - margin-left: 0.5rem !important; -} - -.ms-3 { - margin-left: 1rem !important; -} - -.ms-4 { - margin-left: 1.5rem !important; -} - -.ms-5 { - margin-left: 3rem !important; -} - -.ms-auto { - margin-left: auto !important; -} - -.p-0 { - padding: 0 !important; -} - -.p-1 { - padding: 0.25rem !important; -} - -.p-2 { - padding: 0.5rem !important; -} - -.p-3 { - padding: 1rem !important; -} - -.p-4 { - padding: 1.5rem !important; -} - -.p-5 { - padding: 3rem !important; -} - -.px-0 { - padding-right: 0 !important; - padding-left: 0 !important; -} - -.px-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; -} - -.px-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; -} - -.px-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; -} - -.px-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; -} - -.px-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; -} - -.py-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; -} - -.py-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; -} - -.py-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; -} - -.py-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; -} - -.py-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; -} - -.py-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; -} - -.pt-0 { - padding-top: 0 !important; -} - -.pt-1 { - padding-top: 0.25rem !important; -} - -.pt-2 { - padding-top: 0.5rem !important; -} - -.pt-3 { - padding-top: 1rem !important; -} - -.pt-4 { - padding-top: 1.5rem !important; -} - -.pt-5 { - padding-top: 3rem !important; -} - -.pe-0 { - padding-right: 0 !important; -} - -.pe-1 { - padding-right: 0.25rem !important; -} - -.pe-2 { - padding-right: 0.5rem !important; -} - -.pe-3 { - padding-right: 1rem !important; -} - -.pe-4 { - padding-right: 1.5rem !important; -} - -.pe-5 { - padding-right: 3rem !important; -} - -.pb-0 { - padding-bottom: 0 !important; -} - -.pb-1 { - padding-bottom: 0.25rem !important; -} - -.pb-2 { - padding-bottom: 0.5rem !important; -} - -.pb-3 { - padding-bottom: 1rem !important; -} - -.pb-4 { - padding-bottom: 1.5rem !important; -} - -.pb-5 { - padding-bottom: 3rem !important; -} - -.ps-0 { - padding-left: 0 !important; -} - -.ps-1 { - padding-left: 0.25rem !important; -} - -.ps-2 { - padding-left: 0.5rem !important; -} - -.ps-3 { - padding-left: 1rem !important; -} - -.ps-4 { - padding-left: 1.5rem !important; -} - -.ps-5 { - padding-left: 3rem !important; -} - -.gap-0 { - gap: 0 !important; -} - -.gap-1 { - gap: 0.25rem !important; -} - -.gap-2 { - gap: 0.5rem !important; -} - -.gap-3 { - gap: 1rem !important; -} - -.gap-4 { - gap: 1.5rem !important; -} - -.gap-5 { - gap: 3rem !important; -} - -.row-gap-0 { - row-gap: 0 !important; -} - -.row-gap-1 { - row-gap: 0.25rem !important; -} - -.row-gap-2 { - row-gap: 0.5rem !important; -} - -.row-gap-3 { - row-gap: 1rem !important; -} - -.row-gap-4 { - row-gap: 1.5rem !important; -} - -.row-gap-5 { - row-gap: 3rem !important; -} - -.column-gap-0 { - -moz-column-gap: 0 !important; - column-gap: 0 !important; -} - -.column-gap-1 { - -moz-column-gap: 0.25rem !important; - column-gap: 0.25rem !important; -} - -.column-gap-2 { - -moz-column-gap: 0.5rem !important; - column-gap: 0.5rem !important; -} - -.column-gap-3 { - -moz-column-gap: 1rem !important; - column-gap: 1rem !important; -} - -.column-gap-4 { - -moz-column-gap: 1.5rem !important; - column-gap: 1.5rem !important; -} - -.column-gap-5 { - -moz-column-gap: 3rem !important; - column-gap: 3rem !important; -} - -.font-monospace { - font-family: var(--bs-font-monospace) !important; -} - -.fs-1 { - font-size: calc(1.525rem + 3.3vw) !important; -} - -.fs-2 { - font-size: calc(1.425rem + 2.1vw) !important; -} - -.fs-3 { - font-size: calc(1.375rem + 1.5vw) !important; -} - -.fs-4 { - font-size: calc(1.325rem + 0.9vw) !important; -} - -.fs-5 { - font-size: calc(1.275rem + 0.3vw) !important; -} - -.fs-6 { - font-size: 1rem !important; -} - -.fst-italic { - font-style: italic !important; -} - -.fst-normal { - font-style: normal !important; -} - -.fw-lighter { - font-weight: lighter !important; -} - -.fw-light { - font-weight: 300 !important; -} - -.fw-normal { - font-weight: 400 !important; -} - -.fw-medium { - font-weight: 500 !important; -} - -.fw-semibold { - font-weight: 600 !important; -} - -.fw-bold { - font-weight: 700 !important; -} - -.fw-bolder { - font-weight: bolder !important; -} - -.lh-1 { - line-height: 1 !important; -} - -.lh-sm { - line-height: 1.25 !important; -} - -.lh-base { - line-height: 1.5 !important; -} - -.lh-lg { - line-height: 2 !important; -} - -.text-start { - text-align: left !important; -} - -.text-end { - text-align: right !important; -} - -.text-center { - text-align: center !important; -} - -.text-decoration-none { - text-decoration: none !important; -} - -.text-decoration-underline { - text-decoration: underline !important; -} - -.text-decoration-line-through { - text-decoration: line-through !important; -} - -.text-lowercase { - text-transform: lowercase !important; -} - -.text-uppercase { - text-transform: uppercase !important; -} - -.text-capitalize { - text-transform: capitalize !important; -} - -.text-wrap { - white-space: normal !important; -} - -.text-nowrap { - white-space: nowrap !important; -} - -/* rtl:begin:remove */ -.text-break { - word-wrap: break-word !important; - word-break: break-word !important; -} - -/* rtl:end:remove */ -.text-primary { - --bs-text-opacity: 1; - color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important; -} - -.text-secondary { - --bs-text-opacity: 1; - color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important; -} - -.text-success { - --bs-text-opacity: 1; - color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important; -} - -.text-info { - --bs-text-opacity: 1; - color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important; -} - -.text-warning { - --bs-text-opacity: 1; - color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important; -} - -.text-danger { - --bs-text-opacity: 1; - color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important; -} - -.text-light { - --bs-text-opacity: 1; - color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important; -} - -.text-dark { - --bs-text-opacity: 1; - color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important; -} - -.text-black { - --bs-text-opacity: 1; - color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important; -} - -.text-white { - --bs-text-opacity: 1; - color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important; -} - -.text-body { - --bs-text-opacity: 1; - color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important; -} - -.text-muted { - --bs-text-opacity: 1; - color: var(--bs-secondary-color) !important; -} - -.text-black-50 { - --bs-text-opacity: 1; - color: rgba(0, 0, 0, 0.5) !important; -} - -.text-white-50 { - --bs-text-opacity: 1; - color: rgba(255, 255, 255, 0.5) !important; -} - -.text-body-secondary { - --bs-text-opacity: 1; - color: var(--bs-secondary-color) !important; -} - -.text-body-tertiary { - --bs-text-opacity: 1; - color: var(--bs-tertiary-color) !important; -} - -.text-body-emphasis { - --bs-text-opacity: 1; - color: var(--bs-emphasis-color) !important; -} - -.text-reset { - --bs-text-opacity: 1; - color: inherit !important; -} - -.text-opacity-25 { - --bs-text-opacity: 0.25; -} - -.text-opacity-50 { - --bs-text-opacity: 0.5; -} - -.text-opacity-75 { - --bs-text-opacity: 0.75; -} - -.text-opacity-100 { - --bs-text-opacity: 1; -} - -.text-primary-emphasis { - color: var(--bs-primary-text-emphasis) !important; -} - -.text-secondary-emphasis { - color: var(--bs-secondary-text-emphasis) !important; -} - -.text-success-emphasis { - color: var(--bs-success-text-emphasis) !important; -} - -.text-info-emphasis { - color: var(--bs-info-text-emphasis) !important; -} - -.text-warning-emphasis { - color: var(--bs-warning-text-emphasis) !important; -} - -.text-danger-emphasis { - color: var(--bs-danger-text-emphasis) !important; -} - -.text-light-emphasis { - color: var(--bs-light-text-emphasis) !important; -} - -.text-dark-emphasis { - color: var(--bs-dark-text-emphasis) !important; -} - -.link-opacity-10 { - --bs-link-opacity: 0.1; -} - -.link-opacity-10-hover:hover { - --bs-link-opacity: 0.1; -} - -.link-opacity-25 { - --bs-link-opacity: 0.25; -} - -.link-opacity-25-hover:hover { - --bs-link-opacity: 0.25; -} - -.link-opacity-50 { - --bs-link-opacity: 0.5; -} - -.link-opacity-50-hover:hover { - --bs-link-opacity: 0.5; -} - -.link-opacity-75 { - --bs-link-opacity: 0.75; -} - -.link-opacity-75-hover:hover { - --bs-link-opacity: 0.75; -} - -.link-opacity-100 { - --bs-link-opacity: 1; -} - -.link-opacity-100-hover:hover { - --bs-link-opacity: 1; -} - -.link-offset-1 { - text-underline-offset: 0.125em !important; -} - -.link-offset-1-hover:hover { - text-underline-offset: 0.125em !important; -} - -.link-offset-2 { - text-underline-offset: 0.25em !important; -} - -.link-offset-2-hover:hover { - text-underline-offset: 0.25em !important; -} - -.link-offset-3 { - text-underline-offset: 0.375em !important; -} - -.link-offset-3-hover:hover { - text-underline-offset: 0.375em !important; -} - -.link-underline-primary { - --bs-link-underline-opacity: 1; - -webkit-text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; - text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; -} - -.link-underline-secondary { - --bs-link-underline-opacity: 1; - -webkit-text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; - text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; -} - -.link-underline-success { - --bs-link-underline-opacity: 1; - -webkit-text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; - text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; -} - -.link-underline-info { - --bs-link-underline-opacity: 1; - -webkit-text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; - text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; -} - -.link-underline-warning { - --bs-link-underline-opacity: 1; - -webkit-text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; - text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; -} - -.link-underline-danger { - --bs-link-underline-opacity: 1; - -webkit-text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; - text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; -} - -.link-underline-light { - --bs-link-underline-opacity: 1; - -webkit-text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; - text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; -} - -.link-underline-dark { - --bs-link-underline-opacity: 1; - -webkit-text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; - text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; -} - -.link-underline { - --bs-link-underline-opacity: 1; - -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; - text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; -} - -.link-underline-opacity-0 { - --bs-link-underline-opacity: 0; -} - -.link-underline-opacity-0-hover:hover { - --bs-link-underline-opacity: 0; -} - -.link-underline-opacity-10 { - --bs-link-underline-opacity: 0.1; -} - -.link-underline-opacity-10-hover:hover { - --bs-link-underline-opacity: 0.1; -} - -.link-underline-opacity-25 { - --bs-link-underline-opacity: 0.25; -} - -.link-underline-opacity-25-hover:hover { - --bs-link-underline-opacity: 0.25; -} - -.link-underline-opacity-50 { - --bs-link-underline-opacity: 0.5; -} - -.link-underline-opacity-50-hover:hover { - --bs-link-underline-opacity: 0.5; -} - -.link-underline-opacity-75 { - --bs-link-underline-opacity: 0.75; -} - -.link-underline-opacity-75-hover:hover { - --bs-link-underline-opacity: 0.75; -} - -.link-underline-opacity-100 { - --bs-link-underline-opacity: 1; -} - -.link-underline-opacity-100-hover:hover { - --bs-link-underline-opacity: 1; -} - -.bg-primary { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-secondary { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-success { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-info { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-warning { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-danger { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-light { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-dark { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-black { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-white { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-body { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-transparent { - --bs-bg-opacity: 1; - background-color: transparent !important; -} - -.bg-body-secondary { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-body-tertiary { - --bs-bg-opacity: 1; - background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important; -} - -.bg-opacity-10 { - --bs-bg-opacity: 0.1; -} - -.bg-opacity-25 { - --bs-bg-opacity: 0.25; -} - -.bg-opacity-50 { - --bs-bg-opacity: 0.5; -} - -.bg-opacity-75 { - --bs-bg-opacity: 0.75; -} - -.bg-opacity-100 { - --bs-bg-opacity: 1; -} - -.bg-primary-subtle { - background-color: var(--bs-primary-bg-subtle) !important; -} - -.bg-secondary-subtle { - background-color: var(--bs-secondary-bg-subtle) !important; -} - -.bg-success-subtle { - background-color: var(--bs-success-bg-subtle) !important; -} - -.bg-info-subtle { - background-color: var(--bs-info-bg-subtle) !important; -} - -.bg-warning-subtle { - background-color: var(--bs-warning-bg-subtle) !important; -} - -.bg-danger-subtle { - background-color: var(--bs-danger-bg-subtle) !important; -} - -.bg-light-subtle { - background-color: var(--bs-light-bg-subtle) !important; -} - -.bg-dark-subtle { - background-color: var(--bs-dark-bg-subtle) !important; -} - -.bg-gradient { - background-image: var(--bs-gradient) !important; -} - -.user-select-all { - -webkit-user-select: all !important; - -moz-user-select: all !important; - user-select: all !important; -} - -.user-select-auto { - -webkit-user-select: auto !important; - -moz-user-select: auto !important; - user-select: auto !important; -} - -.user-select-none { - -webkit-user-select: none !important; - -moz-user-select: none !important; - user-select: none !important; -} - -.pe-none { - pointer-events: none !important; -} - -.pe-auto { - pointer-events: auto !important; -} - -.rounded { - border-radius: var(--bs-border-radius) !important; -} - -.rounded-0 { - border-radius: 0 !important; -} - -.rounded-1 { - border-radius: var(--bs-border-radius-sm) !important; -} - -.rounded-2 { - border-radius: var(--bs-border-radius) !important; -} - -.rounded-3 { - border-radius: var(--bs-border-radius-lg) !important; -} - -.rounded-4 { - border-radius: var(--bs-border-radius-xl) !important; -} - -.rounded-5 { - border-radius: var(--bs-border-radius-xxl) !important; -} - -.rounded-circle { - border-radius: 50% !important; -} - -.rounded-pill { - border-radius: var(--bs-border-radius-pill) !important; -} - -.rounded-top { - border-top-left-radius: var(--bs-border-radius) !important; - border-top-right-radius: var(--bs-border-radius) !important; -} - -.rounded-top-0 { - border-top-left-radius: 0 !important; - border-top-right-radius: 0 !important; -} - -.rounded-top-1 { - border-top-left-radius: var(--bs-border-radius-sm) !important; - border-top-right-radius: var(--bs-border-radius-sm) !important; -} - -.rounded-top-2 { - border-top-left-radius: var(--bs-border-radius) !important; - border-top-right-radius: var(--bs-border-radius) !important; -} - -.rounded-top-3 { - border-top-left-radius: var(--bs-border-radius-lg) !important; - border-top-right-radius: var(--bs-border-radius-lg) !important; -} - -.rounded-top-4 { - border-top-left-radius: var(--bs-border-radius-xl) !important; - border-top-right-radius: var(--bs-border-radius-xl) !important; -} - -.rounded-top-5 { - border-top-left-radius: var(--bs-border-radius-xxl) !important; - border-top-right-radius: var(--bs-border-radius-xxl) !important; -} - -.rounded-top-circle { - border-top-left-radius: 50% !important; - border-top-right-radius: 50% !important; -} - -.rounded-top-pill { - border-top-left-radius: var(--bs-border-radius-pill) !important; - border-top-right-radius: var(--bs-border-radius-pill) !important; -} - -.rounded-end { - border-top-right-radius: var(--bs-border-radius) !important; - border-bottom-right-radius: var(--bs-border-radius) !important; -} - -.rounded-end-0 { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; -} - -.rounded-end-1 { - border-top-right-radius: var(--bs-border-radius-sm) !important; - border-bottom-right-radius: var(--bs-border-radius-sm) !important; -} - -.rounded-end-2 { - border-top-right-radius: var(--bs-border-radius) !important; - border-bottom-right-radius: var(--bs-border-radius) !important; -} - -.rounded-end-3 { - border-top-right-radius: var(--bs-border-radius-lg) !important; - border-bottom-right-radius: var(--bs-border-radius-lg) !important; -} - -.rounded-end-4 { - border-top-right-radius: var(--bs-border-radius-xl) !important; - border-bottom-right-radius: var(--bs-border-radius-xl) !important; -} - -.rounded-end-5 { - border-top-right-radius: var(--bs-border-radius-xxl) !important; - border-bottom-right-radius: var(--bs-border-radius-xxl) !important; -} - -.rounded-end-circle { - border-top-right-radius: 50% !important; - border-bottom-right-radius: 50% !important; -} - -.rounded-end-pill { - border-top-right-radius: var(--bs-border-radius-pill) !important; - border-bottom-right-radius: var(--bs-border-radius-pill) !important; -} - -.rounded-bottom { - border-bottom-right-radius: var(--bs-border-radius) !important; - border-bottom-left-radius: var(--bs-border-radius) !important; -} - -.rounded-bottom-0 { - border-bottom-right-radius: 0 !important; - border-bottom-left-radius: 0 !important; -} - -.rounded-bottom-1 { - border-bottom-right-radius: var(--bs-border-radius-sm) !important; - border-bottom-left-radius: var(--bs-border-radius-sm) !important; -} - -.rounded-bottom-2 { - border-bottom-right-radius: var(--bs-border-radius) !important; - border-bottom-left-radius: var(--bs-border-radius) !important; -} - -.rounded-bottom-3 { - border-bottom-right-radius: var(--bs-border-radius-lg) !important; - border-bottom-left-radius: var(--bs-border-radius-lg) !important; -} - -.rounded-bottom-4 { - border-bottom-right-radius: var(--bs-border-radius-xl) !important; - border-bottom-left-radius: var(--bs-border-radius-xl) !important; -} - -.rounded-bottom-5 { - border-bottom-right-radius: var(--bs-border-radius-xxl) !important; - border-bottom-left-radius: var(--bs-border-radius-xxl) !important; -} - -.rounded-bottom-circle { - border-bottom-right-radius: 50% !important; - border-bottom-left-radius: 50% !important; -} - -.rounded-bottom-pill { - border-bottom-right-radius: var(--bs-border-radius-pill) !important; - border-bottom-left-radius: var(--bs-border-radius-pill) !important; -} - -.rounded-start { - border-bottom-left-radius: var(--bs-border-radius) !important; - border-top-left-radius: var(--bs-border-radius) !important; -} - -.rounded-start-0 { - border-bottom-left-radius: 0 !important; - border-top-left-radius: 0 !important; -} - -.rounded-start-1 { - border-bottom-left-radius: var(--bs-border-radius-sm) !important; - border-top-left-radius: var(--bs-border-radius-sm) !important; -} - -.rounded-start-2 { - border-bottom-left-radius: var(--bs-border-radius) !important; - border-top-left-radius: var(--bs-border-radius) !important; -} - -.rounded-start-3 { - border-bottom-left-radius: var(--bs-border-radius-lg) !important; - border-top-left-radius: var(--bs-border-radius-lg) !important; -} - -.rounded-start-4 { - border-bottom-left-radius: var(--bs-border-radius-xl) !important; - border-top-left-radius: var(--bs-border-radius-xl) !important; -} - -.rounded-start-5 { - border-bottom-left-radius: var(--bs-border-radius-xxl) !important; - border-top-left-radius: var(--bs-border-radius-xxl) !important; -} - -.rounded-start-circle { - border-bottom-left-radius: 50% !important; - border-top-left-radius: 50% !important; -} - -.rounded-start-pill { - border-bottom-left-radius: var(--bs-border-radius-pill) !important; - border-top-left-radius: var(--bs-border-radius-pill) !important; -} - -.visible { - visibility: visible !important; -} - -.invisible { - visibility: hidden !important; -} - -.z-n1 { - z-index: -1 !important; -} - -.z-0 { - z-index: 0 !important; -} - -.z-1 { - z-index: 1 !important; -} - -.z-2 { - z-index: 2 !important; -} - -.z-3 { - z-index: 3 !important; -} - -@media (min-width: 576px) { - .float-sm-start { - float: left !important; - } - .float-sm-end { - float: right !important; - } - .float-sm-none { - float: none !important; - } - .object-fit-sm-contain { - -o-object-fit: contain !important; - object-fit: contain !important; - } - .object-fit-sm-cover { - -o-object-fit: cover !important; - object-fit: cover !important; - } - .object-fit-sm-fill { - -o-object-fit: fill !important; - object-fit: fill !important; - } - .object-fit-sm-scale { - -o-object-fit: scale-down !important; - object-fit: scale-down !important; - } - .object-fit-sm-none { - -o-object-fit: none !important; - object-fit: none !important; - } - .d-sm-inline { - display: inline !important; - } - .d-sm-inline-block { - display: inline-block !important; - } - .d-sm-block { - display: block !important; - } - .d-sm-grid { - display: grid !important; - } - .d-sm-inline-grid { - display: inline-grid !important; - } - .d-sm-table { - display: table !important; - } - .d-sm-table-row { - display: table-row !important; - } - .d-sm-table-cell { - display: table-cell !important; - } - .d-sm-flex { - display: flex !important; - } - .d-sm-inline-flex { - display: inline-flex !important; - } - .d-sm-none { - display: none !important; - } - .flex-sm-fill { - flex: 1 1 auto !important; - } - .flex-sm-row { - flex-direction: row !important; - } - .flex-sm-column { - flex-direction: column !important; - } - .flex-sm-row-reverse { - flex-direction: row-reverse !important; - } - .flex-sm-column-reverse { - flex-direction: column-reverse !important; - } - .flex-sm-grow-0 { - flex-grow: 0 !important; - } - .flex-sm-grow-1 { - flex-grow: 1 !important; - } - .flex-sm-shrink-0 { - flex-shrink: 0 !important; - } - .flex-sm-shrink-1 { - flex-shrink: 1 !important; - } - .flex-sm-wrap { - flex-wrap: wrap !important; - } - .flex-sm-nowrap { - flex-wrap: nowrap !important; - } - .flex-sm-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - .justify-content-sm-start { - justify-content: flex-start !important; - } - .justify-content-sm-end { - justify-content: flex-end !important; - } - .justify-content-sm-center { - justify-content: center !important; - } - .justify-content-sm-between { - justify-content: space-between !important; - } - .justify-content-sm-around { - justify-content: space-around !important; - } - .justify-content-sm-evenly { - justify-content: space-evenly !important; - } - .align-items-sm-start { - align-items: flex-start !important; - } - .align-items-sm-end { - align-items: flex-end !important; - } - .align-items-sm-center { - align-items: center !important; - } - .align-items-sm-baseline { - align-items: baseline !important; - } - .align-items-sm-stretch { - align-items: stretch !important; - } - .align-content-sm-start { - align-content: flex-start !important; - } - .align-content-sm-end { - align-content: flex-end !important; - } - .align-content-sm-center { - align-content: center !important; - } - .align-content-sm-between { - align-content: space-between !important; - } - .align-content-sm-around { - align-content: space-around !important; - } - .align-content-sm-stretch { - align-content: stretch !important; - } - .align-self-sm-auto { - align-self: auto !important; - } - .align-self-sm-start { - align-self: flex-start !important; - } - .align-self-sm-end { - align-self: flex-end !important; - } - .align-self-sm-center { - align-self: center !important; - } - .align-self-sm-baseline { - align-self: baseline !important; - } - .align-self-sm-stretch { - align-self: stretch !important; - } - .order-sm-first { - order: -1 !important; - } - .order-sm-0 { - order: 0 !important; - } - .order-sm-1 { - order: 1 !important; - } - .order-sm-2 { - order: 2 !important; - } - .order-sm-3 { - order: 3 !important; - } - .order-sm-4 { - order: 4 !important; - } - .order-sm-5 { - order: 5 !important; - } - .order-sm-last { - order: 6 !important; - } - .m-sm-0 { - margin: 0 !important; - } - .m-sm-1 { - margin: 0.25rem !important; - } - .m-sm-2 { - margin: 0.5rem !important; - } - .m-sm-3 { - margin: 1rem !important; - } - .m-sm-4 { - margin: 1.5rem !important; - } - .m-sm-5 { - margin: 3rem !important; - } - .m-sm-auto { - margin: auto !important; - } - .mx-sm-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - .mx-sm-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - .mx-sm-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - .mx-sm-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - .mx-sm-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - .mx-sm-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - .mx-sm-auto { - margin-right: auto !important; - margin-left: auto !important; - } - .my-sm-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - .my-sm-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - .my-sm-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - .my-sm-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - .my-sm-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - .my-sm-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - .my-sm-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - .mt-sm-0 { - margin-top: 0 !important; - } - .mt-sm-1 { - margin-top: 0.25rem !important; - } - .mt-sm-2 { - margin-top: 0.5rem !important; - } - .mt-sm-3 { - margin-top: 1rem !important; - } - .mt-sm-4 { - margin-top: 1.5rem !important; - } - .mt-sm-5 { - margin-top: 3rem !important; - } - .mt-sm-auto { - margin-top: auto !important; - } - .me-sm-0 { - margin-right: 0 !important; - } - .me-sm-1 { - margin-right: 0.25rem !important; - } - .me-sm-2 { - margin-right: 0.5rem !important; - } - .me-sm-3 { - margin-right: 1rem !important; - } - .me-sm-4 { - margin-right: 1.5rem !important; - } - .me-sm-5 { - margin-right: 3rem !important; - } - .me-sm-auto { - margin-right: auto !important; - } - .mb-sm-0 { - margin-bottom: 0 !important; - } - .mb-sm-1 { - margin-bottom: 0.25rem !important; - } - .mb-sm-2 { - margin-bottom: 0.5rem !important; - } - .mb-sm-3 { - margin-bottom: 1rem !important; - } - .mb-sm-4 { - margin-bottom: 1.5rem !important; - } - .mb-sm-5 { - margin-bottom: 3rem !important; - } - .mb-sm-auto { - margin-bottom: auto !important; - } - .ms-sm-0 { - margin-left: 0 !important; - } - .ms-sm-1 { - margin-left: 0.25rem !important; - } - .ms-sm-2 { - margin-left: 0.5rem !important; - } - .ms-sm-3 { - margin-left: 1rem !important; - } - .ms-sm-4 { - margin-left: 1.5rem !important; - } - .ms-sm-5 { - margin-left: 3rem !important; - } - .ms-sm-auto { - margin-left: auto !important; - } - .p-sm-0 { - padding: 0 !important; - } - .p-sm-1 { - padding: 0.25rem !important; - } - .p-sm-2 { - padding: 0.5rem !important; - } - .p-sm-3 { - padding: 1rem !important; - } - .p-sm-4 { - padding: 1.5rem !important; - } - .p-sm-5 { - padding: 3rem !important; - } - .px-sm-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - .px-sm-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - .px-sm-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - .px-sm-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - .px-sm-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - .px-sm-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - .py-sm-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - .py-sm-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - .py-sm-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - .py-sm-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - .py-sm-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - .py-sm-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - .pt-sm-0 { - padding-top: 0 !important; - } - .pt-sm-1 { - padding-top: 0.25rem !important; - } - .pt-sm-2 { - padding-top: 0.5rem !important; - } - .pt-sm-3 { - padding-top: 1rem !important; - } - .pt-sm-4 { - padding-top: 1.5rem !important; - } - .pt-sm-5 { - padding-top: 3rem !important; - } - .pe-sm-0 { - padding-right: 0 !important; - } - .pe-sm-1 { - padding-right: 0.25rem !important; - } - .pe-sm-2 { - padding-right: 0.5rem !important; - } - .pe-sm-3 { - padding-right: 1rem !important; - } - .pe-sm-4 { - padding-right: 1.5rem !important; - } - .pe-sm-5 { - padding-right: 3rem !important; - } - .pb-sm-0 { - padding-bottom: 0 !important; - } - .pb-sm-1 { - padding-bottom: 0.25rem !important; - } - .pb-sm-2 { - padding-bottom: 0.5rem !important; - } - .pb-sm-3 { - padding-bottom: 1rem !important; - } - .pb-sm-4 { - padding-bottom: 1.5rem !important; - } - .pb-sm-5 { - padding-bottom: 3rem !important; - } - .ps-sm-0 { - padding-left: 0 !important; - } - .ps-sm-1 { - padding-left: 0.25rem !important; - } - .ps-sm-2 { - padding-left: 0.5rem !important; - } - .ps-sm-3 { - padding-left: 1rem !important; - } - .ps-sm-4 { - padding-left: 1.5rem !important; - } - .ps-sm-5 { - padding-left: 3rem !important; - } - .gap-sm-0 { - gap: 0 !important; - } - .gap-sm-1 { - gap: 0.25rem !important; - } - .gap-sm-2 { - gap: 0.5rem !important; - } - .gap-sm-3 { - gap: 1rem !important; - } - .gap-sm-4 { - gap: 1.5rem !important; - } - .gap-sm-5 { - gap: 3rem !important; - } - .row-gap-sm-0 { - row-gap: 0 !important; - } - .row-gap-sm-1 { - row-gap: 0.25rem !important; - } - .row-gap-sm-2 { - row-gap: 0.5rem !important; - } - .row-gap-sm-3 { - row-gap: 1rem !important; - } - .row-gap-sm-4 { - row-gap: 1.5rem !important; - } - .row-gap-sm-5 { - row-gap: 3rem !important; - } - .column-gap-sm-0 { - -moz-column-gap: 0 !important; - column-gap: 0 !important; - } - .column-gap-sm-1 { - -moz-column-gap: 0.25rem !important; - column-gap: 0.25rem !important; - } - .column-gap-sm-2 { - -moz-column-gap: 0.5rem !important; - column-gap: 0.5rem !important; - } - .column-gap-sm-3 { - -moz-column-gap: 1rem !important; - column-gap: 1rem !important; - } - .column-gap-sm-4 { - -moz-column-gap: 1.5rem !important; - column-gap: 1.5rem !important; - } - .column-gap-sm-5 { - -moz-column-gap: 3rem !important; - column-gap: 3rem !important; - } - .text-sm-start { - text-align: left !important; - } - .text-sm-end { - text-align: right !important; - } - .text-sm-center { - text-align: center !important; - } -} -@media (min-width: 768px) { - .float-md-start { - float: left !important; - } - .float-md-end { - float: right !important; - } - .float-md-none { - float: none !important; - } - .object-fit-md-contain { - -o-object-fit: contain !important; - object-fit: contain !important; - } - .object-fit-md-cover { - -o-object-fit: cover !important; - object-fit: cover !important; - } - .object-fit-md-fill { - -o-object-fit: fill !important; - object-fit: fill !important; - } - .object-fit-md-scale { - -o-object-fit: scale-down !important; - object-fit: scale-down !important; - } - .object-fit-md-none { - -o-object-fit: none !important; - object-fit: none !important; - } - .d-md-inline { - display: inline !important; - } - .d-md-inline-block { - display: inline-block !important; - } - .d-md-block { - display: block !important; - } - .d-md-grid { - display: grid !important; - } - .d-md-inline-grid { - display: inline-grid !important; - } - .d-md-table { - display: table !important; - } - .d-md-table-row { - display: table-row !important; - } - .d-md-table-cell { - display: table-cell !important; - } - .d-md-flex { - display: flex !important; - } - .d-md-inline-flex { - display: inline-flex !important; - } - .d-md-none { - display: none !important; - } - .flex-md-fill { - flex: 1 1 auto !important; - } - .flex-md-row { - flex-direction: row !important; - } - .flex-md-column { - flex-direction: column !important; - } - .flex-md-row-reverse { - flex-direction: row-reverse !important; - } - .flex-md-column-reverse { - flex-direction: column-reverse !important; - } - .flex-md-grow-0 { - flex-grow: 0 !important; - } - .flex-md-grow-1 { - flex-grow: 1 !important; - } - .flex-md-shrink-0 { - flex-shrink: 0 !important; - } - .flex-md-shrink-1 { - flex-shrink: 1 !important; - } - .flex-md-wrap { - flex-wrap: wrap !important; - } - .flex-md-nowrap { - flex-wrap: nowrap !important; - } - .flex-md-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - .justify-content-md-start { - justify-content: flex-start !important; - } - .justify-content-md-end { - justify-content: flex-end !important; - } - .justify-content-md-center { - justify-content: center !important; - } - .justify-content-md-between { - justify-content: space-between !important; - } - .justify-content-md-around { - justify-content: space-around !important; - } - .justify-content-md-evenly { - justify-content: space-evenly !important; - } - .align-items-md-start { - align-items: flex-start !important; - } - .align-items-md-end { - align-items: flex-end !important; - } - .align-items-md-center { - align-items: center !important; - } - .align-items-md-baseline { - align-items: baseline !important; - } - .align-items-md-stretch { - align-items: stretch !important; - } - .align-content-md-start { - align-content: flex-start !important; - } - .align-content-md-end { - align-content: flex-end !important; - } - .align-content-md-center { - align-content: center !important; - } - .align-content-md-between { - align-content: space-between !important; - } - .align-content-md-around { - align-content: space-around !important; - } - .align-content-md-stretch { - align-content: stretch !important; - } - .align-self-md-auto { - align-self: auto !important; - } - .align-self-md-start { - align-self: flex-start !important; - } - .align-self-md-end { - align-self: flex-end !important; - } - .align-self-md-center { - align-self: center !important; - } - .align-self-md-baseline { - align-self: baseline !important; - } - .align-self-md-stretch { - align-self: stretch !important; - } - .order-md-first { - order: -1 !important; - } - .order-md-0 { - order: 0 !important; - } - .order-md-1 { - order: 1 !important; - } - .order-md-2 { - order: 2 !important; - } - .order-md-3 { - order: 3 !important; - } - .order-md-4 { - order: 4 !important; - } - .order-md-5 { - order: 5 !important; - } - .order-md-last { - order: 6 !important; - } - .m-md-0 { - margin: 0 !important; - } - .m-md-1 { - margin: 0.25rem !important; - } - .m-md-2 { - margin: 0.5rem !important; - } - .m-md-3 { - margin: 1rem !important; - } - .m-md-4 { - margin: 1.5rem !important; - } - .m-md-5 { - margin: 3rem !important; - } - .m-md-auto { - margin: auto !important; - } - .mx-md-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - .mx-md-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - .mx-md-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - .mx-md-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - .mx-md-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - .mx-md-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - .mx-md-auto { - margin-right: auto !important; - margin-left: auto !important; - } - .my-md-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - .my-md-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - .my-md-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - .my-md-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - .my-md-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - .my-md-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - .my-md-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - .mt-md-0 { - margin-top: 0 !important; - } - .mt-md-1 { - margin-top: 0.25rem !important; - } - .mt-md-2 { - margin-top: 0.5rem !important; - } - .mt-md-3 { - margin-top: 1rem !important; - } - .mt-md-4 { - margin-top: 1.5rem !important; - } - .mt-md-5 { - margin-top: 3rem !important; - } - .mt-md-auto { - margin-top: auto !important; - } - .me-md-0 { - margin-right: 0 !important; - } - .me-md-1 { - margin-right: 0.25rem !important; - } - .me-md-2 { - margin-right: 0.5rem !important; - } - .me-md-3 { - margin-right: 1rem !important; - } - .me-md-4 { - margin-right: 1.5rem !important; - } - .me-md-5 { - margin-right: 3rem !important; - } - .me-md-auto { - margin-right: auto !important; - } - .mb-md-0 { - margin-bottom: 0 !important; - } - .mb-md-1 { - margin-bottom: 0.25rem !important; - } - .mb-md-2 { - margin-bottom: 0.5rem !important; - } - .mb-md-3 { - margin-bottom: 1rem !important; - } - .mb-md-4 { - margin-bottom: 1.5rem !important; - } - .mb-md-5 { - margin-bottom: 3rem !important; - } - .mb-md-auto { - margin-bottom: auto !important; - } - .ms-md-0 { - margin-left: 0 !important; - } - .ms-md-1 { - margin-left: 0.25rem !important; - } - .ms-md-2 { - margin-left: 0.5rem !important; - } - .ms-md-3 { - margin-left: 1rem !important; - } - .ms-md-4 { - margin-left: 1.5rem !important; - } - .ms-md-5 { - margin-left: 3rem !important; - } - .ms-md-auto { - margin-left: auto !important; - } - .p-md-0 { - padding: 0 !important; - } - .p-md-1 { - padding: 0.25rem !important; - } - .p-md-2 { - padding: 0.5rem !important; - } - .p-md-3 { - padding: 1rem !important; - } - .p-md-4 { - padding: 1.5rem !important; - } - .p-md-5 { - padding: 3rem !important; - } - .px-md-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - .px-md-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - .px-md-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - .px-md-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - .px-md-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - .px-md-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - .py-md-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - .py-md-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - .py-md-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - .py-md-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - .py-md-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - .py-md-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - .pt-md-0 { - padding-top: 0 !important; - } - .pt-md-1 { - padding-top: 0.25rem !important; - } - .pt-md-2 { - padding-top: 0.5rem !important; - } - .pt-md-3 { - padding-top: 1rem !important; - } - .pt-md-4 { - padding-top: 1.5rem !important; - } - .pt-md-5 { - padding-top: 3rem !important; - } - .pe-md-0 { - padding-right: 0 !important; - } - .pe-md-1 { - padding-right: 0.25rem !important; - } - .pe-md-2 { - padding-right: 0.5rem !important; - } - .pe-md-3 { - padding-right: 1rem !important; - } - .pe-md-4 { - padding-right: 1.5rem !important; - } - .pe-md-5 { - padding-right: 3rem !important; - } - .pb-md-0 { - padding-bottom: 0 !important; - } - .pb-md-1 { - padding-bottom: 0.25rem !important; - } - .pb-md-2 { - padding-bottom: 0.5rem !important; - } - .pb-md-3 { - padding-bottom: 1rem !important; - } - .pb-md-4 { - padding-bottom: 1.5rem !important; - } - .pb-md-5 { - padding-bottom: 3rem !important; - } - .ps-md-0 { - padding-left: 0 !important; - } - .ps-md-1 { - padding-left: 0.25rem !important; - } - .ps-md-2 { - padding-left: 0.5rem !important; - } - .ps-md-3 { - padding-left: 1rem !important; - } - .ps-md-4 { - padding-left: 1.5rem !important; - } - .ps-md-5 { - padding-left: 3rem !important; - } - .gap-md-0 { - gap: 0 !important; - } - .gap-md-1 { - gap: 0.25rem !important; - } - .gap-md-2 { - gap: 0.5rem !important; - } - .gap-md-3 { - gap: 1rem !important; - } - .gap-md-4 { - gap: 1.5rem !important; - } - .gap-md-5 { - gap: 3rem !important; - } - .row-gap-md-0 { - row-gap: 0 !important; - } - .row-gap-md-1 { - row-gap: 0.25rem !important; - } - .row-gap-md-2 { - row-gap: 0.5rem !important; - } - .row-gap-md-3 { - row-gap: 1rem !important; - } - .row-gap-md-4 { - row-gap: 1.5rem !important; - } - .row-gap-md-5 { - row-gap: 3rem !important; - } - .column-gap-md-0 { - -moz-column-gap: 0 !important; - column-gap: 0 !important; - } - .column-gap-md-1 { - -moz-column-gap: 0.25rem !important; - column-gap: 0.25rem !important; - } - .column-gap-md-2 { - -moz-column-gap: 0.5rem !important; - column-gap: 0.5rem !important; - } - .column-gap-md-3 { - -moz-column-gap: 1rem !important; - column-gap: 1rem !important; - } - .column-gap-md-4 { - -moz-column-gap: 1.5rem !important; - column-gap: 1.5rem !important; - } - .column-gap-md-5 { - -moz-column-gap: 3rem !important; - column-gap: 3rem !important; - } - .text-md-start { - text-align: left !important; - } - .text-md-end { - text-align: right !important; - } - .text-md-center { - text-align: center !important; - } -} -@media (min-width: 992px) { - .float-lg-start { - float: left !important; - } - .float-lg-end { - float: right !important; - } - .float-lg-none { - float: none !important; - } - .object-fit-lg-contain { - -o-object-fit: contain !important; - object-fit: contain !important; - } - .object-fit-lg-cover { - -o-object-fit: cover !important; - object-fit: cover !important; - } - .object-fit-lg-fill { - -o-object-fit: fill !important; - object-fit: fill !important; - } - .object-fit-lg-scale { - -o-object-fit: scale-down !important; - object-fit: scale-down !important; - } - .object-fit-lg-none { - -o-object-fit: none !important; - object-fit: none !important; - } - .d-lg-inline { - display: inline !important; - } - .d-lg-inline-block { - display: inline-block !important; - } - .d-lg-block { - display: block !important; - } - .d-lg-grid { - display: grid !important; - } - .d-lg-inline-grid { - display: inline-grid !important; - } - .d-lg-table { - display: table !important; - } - .d-lg-table-row { - display: table-row !important; - } - .d-lg-table-cell { - display: table-cell !important; - } - .d-lg-flex { - display: flex !important; - } - .d-lg-inline-flex { - display: inline-flex !important; - } - .d-lg-none { - display: none !important; - } - .flex-lg-fill { - flex: 1 1 auto !important; - } - .flex-lg-row { - flex-direction: row !important; - } - .flex-lg-column { - flex-direction: column !important; - } - .flex-lg-row-reverse { - flex-direction: row-reverse !important; - } - .flex-lg-column-reverse { - flex-direction: column-reverse !important; - } - .flex-lg-grow-0 { - flex-grow: 0 !important; - } - .flex-lg-grow-1 { - flex-grow: 1 !important; - } - .flex-lg-shrink-0 { - flex-shrink: 0 !important; - } - .flex-lg-shrink-1 { - flex-shrink: 1 !important; - } - .flex-lg-wrap { - flex-wrap: wrap !important; - } - .flex-lg-nowrap { - flex-wrap: nowrap !important; - } - .flex-lg-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - .justify-content-lg-start { - justify-content: flex-start !important; - } - .justify-content-lg-end { - justify-content: flex-end !important; - } - .justify-content-lg-center { - justify-content: center !important; - } - .justify-content-lg-between { - justify-content: space-between !important; - } - .justify-content-lg-around { - justify-content: space-around !important; - } - .justify-content-lg-evenly { - justify-content: space-evenly !important; - } - .align-items-lg-start { - align-items: flex-start !important; - } - .align-items-lg-end { - align-items: flex-end !important; - } - .align-items-lg-center { - align-items: center !important; - } - .align-items-lg-baseline { - align-items: baseline !important; - } - .align-items-lg-stretch { - align-items: stretch !important; - } - .align-content-lg-start { - align-content: flex-start !important; - } - .align-content-lg-end { - align-content: flex-end !important; - } - .align-content-lg-center { - align-content: center !important; - } - .align-content-lg-between { - align-content: space-between !important; - } - .align-content-lg-around { - align-content: space-around !important; - } - .align-content-lg-stretch { - align-content: stretch !important; - } - .align-self-lg-auto { - align-self: auto !important; - } - .align-self-lg-start { - align-self: flex-start !important; - } - .align-self-lg-end { - align-self: flex-end !important; - } - .align-self-lg-center { - align-self: center !important; - } - .align-self-lg-baseline { - align-self: baseline !important; - } - .align-self-lg-stretch { - align-self: stretch !important; - } - .order-lg-first { - order: -1 !important; - } - .order-lg-0 { - order: 0 !important; - } - .order-lg-1 { - order: 1 !important; - } - .order-lg-2 { - order: 2 !important; - } - .order-lg-3 { - order: 3 !important; - } - .order-lg-4 { - order: 4 !important; - } - .order-lg-5 { - order: 5 !important; - } - .order-lg-last { - order: 6 !important; - } - .m-lg-0 { - margin: 0 !important; - } - .m-lg-1 { - margin: 0.25rem !important; - } - .m-lg-2 { - margin: 0.5rem !important; - } - .m-lg-3 { - margin: 1rem !important; - } - .m-lg-4 { - margin: 1.5rem !important; - } - .m-lg-5 { - margin: 3rem !important; - } - .m-lg-auto { - margin: auto !important; - } - .mx-lg-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - .mx-lg-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - .mx-lg-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - .mx-lg-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - .mx-lg-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - .mx-lg-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - .mx-lg-auto { - margin-right: auto !important; - margin-left: auto !important; - } - .my-lg-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - .my-lg-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - .my-lg-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - .my-lg-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - .my-lg-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - .my-lg-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - .my-lg-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - .mt-lg-0 { - margin-top: 0 !important; - } - .mt-lg-1 { - margin-top: 0.25rem !important; - } - .mt-lg-2 { - margin-top: 0.5rem !important; - } - .mt-lg-3 { - margin-top: 1rem !important; - } - .mt-lg-4 { - margin-top: 1.5rem !important; - } - .mt-lg-5 { - margin-top: 3rem !important; - } - .mt-lg-auto { - margin-top: auto !important; - } - .me-lg-0 { - margin-right: 0 !important; - } - .me-lg-1 { - margin-right: 0.25rem !important; - } - .me-lg-2 { - margin-right: 0.5rem !important; - } - .me-lg-3 { - margin-right: 1rem !important; - } - .me-lg-4 { - margin-right: 1.5rem !important; - } - .me-lg-5 { - margin-right: 3rem !important; - } - .me-lg-auto { - margin-right: auto !important; - } - .mb-lg-0 { - margin-bottom: 0 !important; - } - .mb-lg-1 { - margin-bottom: 0.25rem !important; - } - .mb-lg-2 { - margin-bottom: 0.5rem !important; - } - .mb-lg-3 { - margin-bottom: 1rem !important; - } - .mb-lg-4 { - margin-bottom: 1.5rem !important; - } - .mb-lg-5 { - margin-bottom: 3rem !important; - } - .mb-lg-auto { - margin-bottom: auto !important; - } - .ms-lg-0 { - margin-left: 0 !important; - } - .ms-lg-1 { - margin-left: 0.25rem !important; - } - .ms-lg-2 { - margin-left: 0.5rem !important; - } - .ms-lg-3 { - margin-left: 1rem !important; - } - .ms-lg-4 { - margin-left: 1.5rem !important; - } - .ms-lg-5 { - margin-left: 3rem !important; - } - .ms-lg-auto { - margin-left: auto !important; - } - .p-lg-0 { - padding: 0 !important; - } - .p-lg-1 { - padding: 0.25rem !important; - } - .p-lg-2 { - padding: 0.5rem !important; - } - .p-lg-3 { - padding: 1rem !important; - } - .p-lg-4 { - padding: 1.5rem !important; - } - .p-lg-5 { - padding: 3rem !important; - } - .px-lg-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - .px-lg-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - .px-lg-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - .px-lg-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - .px-lg-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - .px-lg-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - .py-lg-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - .py-lg-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - .py-lg-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - .py-lg-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - .py-lg-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - .py-lg-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - .pt-lg-0 { - padding-top: 0 !important; - } - .pt-lg-1 { - padding-top: 0.25rem !important; - } - .pt-lg-2 { - padding-top: 0.5rem !important; - } - .pt-lg-3 { - padding-top: 1rem !important; - } - .pt-lg-4 { - padding-top: 1.5rem !important; - } - .pt-lg-5 { - padding-top: 3rem !important; - } - .pe-lg-0 { - padding-right: 0 !important; - } - .pe-lg-1 { - padding-right: 0.25rem !important; - } - .pe-lg-2 { - padding-right: 0.5rem !important; - } - .pe-lg-3 { - padding-right: 1rem !important; - } - .pe-lg-4 { - padding-right: 1.5rem !important; - } - .pe-lg-5 { - padding-right: 3rem !important; - } - .pb-lg-0 { - padding-bottom: 0 !important; - } - .pb-lg-1 { - padding-bottom: 0.25rem !important; - } - .pb-lg-2 { - padding-bottom: 0.5rem !important; - } - .pb-lg-3 { - padding-bottom: 1rem !important; - } - .pb-lg-4 { - padding-bottom: 1.5rem !important; - } - .pb-lg-5 { - padding-bottom: 3rem !important; - } - .ps-lg-0 { - padding-left: 0 !important; - } - .ps-lg-1 { - padding-left: 0.25rem !important; - } - .ps-lg-2 { - padding-left: 0.5rem !important; - } - .ps-lg-3 { - padding-left: 1rem !important; - } - .ps-lg-4 { - padding-left: 1.5rem !important; - } - .ps-lg-5 { - padding-left: 3rem !important; - } - .gap-lg-0 { - gap: 0 !important; - } - .gap-lg-1 { - gap: 0.25rem !important; - } - .gap-lg-2 { - gap: 0.5rem !important; - } - .gap-lg-3 { - gap: 1rem !important; - } - .gap-lg-4 { - gap: 1.5rem !important; - } - .gap-lg-5 { - gap: 3rem !important; - } - .row-gap-lg-0 { - row-gap: 0 !important; - } - .row-gap-lg-1 { - row-gap: 0.25rem !important; - } - .row-gap-lg-2 { - row-gap: 0.5rem !important; - } - .row-gap-lg-3 { - row-gap: 1rem !important; - } - .row-gap-lg-4 { - row-gap: 1.5rem !important; - } - .row-gap-lg-5 { - row-gap: 3rem !important; - } - .column-gap-lg-0 { - -moz-column-gap: 0 !important; - column-gap: 0 !important; - } - .column-gap-lg-1 { - -moz-column-gap: 0.25rem !important; - column-gap: 0.25rem !important; - } - .column-gap-lg-2 { - -moz-column-gap: 0.5rem !important; - column-gap: 0.5rem !important; - } - .column-gap-lg-3 { - -moz-column-gap: 1rem !important; - column-gap: 1rem !important; - } - .column-gap-lg-4 { - -moz-column-gap: 1.5rem !important; - column-gap: 1.5rem !important; - } - .column-gap-lg-5 { - -moz-column-gap: 3rem !important; - column-gap: 3rem !important; - } - .text-lg-start { - text-align: left !important; - } - .text-lg-end { - text-align: right !important; - } - .text-lg-center { - text-align: center !important; - } -} -@media (min-width: 1200px) { - .float-xl-start { - float: left !important; - } - .float-xl-end { - float: right !important; - } - .float-xl-none { - float: none !important; - } - .object-fit-xl-contain { - -o-object-fit: contain !important; - object-fit: contain !important; - } - .object-fit-xl-cover { - -o-object-fit: cover !important; - object-fit: cover !important; - } - .object-fit-xl-fill { - -o-object-fit: fill !important; - object-fit: fill !important; - } - .object-fit-xl-scale { - -o-object-fit: scale-down !important; - object-fit: scale-down !important; - } - .object-fit-xl-none { - -o-object-fit: none !important; - object-fit: none !important; - } - .d-xl-inline { - display: inline !important; - } - .d-xl-inline-block { - display: inline-block !important; - } - .d-xl-block { - display: block !important; - } - .d-xl-grid { - display: grid !important; - } - .d-xl-inline-grid { - display: inline-grid !important; - } - .d-xl-table { - display: table !important; - } - .d-xl-table-row { - display: table-row !important; - } - .d-xl-table-cell { - display: table-cell !important; - } - .d-xl-flex { - display: flex !important; - } - .d-xl-inline-flex { - display: inline-flex !important; - } - .d-xl-none { - display: none !important; - } - .flex-xl-fill { - flex: 1 1 auto !important; - } - .flex-xl-row { - flex-direction: row !important; - } - .flex-xl-column { - flex-direction: column !important; - } - .flex-xl-row-reverse { - flex-direction: row-reverse !important; - } - .flex-xl-column-reverse { - flex-direction: column-reverse !important; - } - .flex-xl-grow-0 { - flex-grow: 0 !important; - } - .flex-xl-grow-1 { - flex-grow: 1 !important; - } - .flex-xl-shrink-0 { - flex-shrink: 0 !important; - } - .flex-xl-shrink-1 { - flex-shrink: 1 !important; - } - .flex-xl-wrap { - flex-wrap: wrap !important; - } - .flex-xl-nowrap { - flex-wrap: nowrap !important; - } - .flex-xl-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - .justify-content-xl-start { - justify-content: flex-start !important; - } - .justify-content-xl-end { - justify-content: flex-end !important; - } - .justify-content-xl-center { - justify-content: center !important; - } - .justify-content-xl-between { - justify-content: space-between !important; - } - .justify-content-xl-around { - justify-content: space-around !important; - } - .justify-content-xl-evenly { - justify-content: space-evenly !important; - } - .align-items-xl-start { - align-items: flex-start !important; - } - .align-items-xl-end { - align-items: flex-end !important; - } - .align-items-xl-center { - align-items: center !important; - } - .align-items-xl-baseline { - align-items: baseline !important; - } - .align-items-xl-stretch { - align-items: stretch !important; - } - .align-content-xl-start { - align-content: flex-start !important; - } - .align-content-xl-end { - align-content: flex-end !important; - } - .align-content-xl-center { - align-content: center !important; - } - .align-content-xl-between { - align-content: space-between !important; - } - .align-content-xl-around { - align-content: space-around !important; - } - .align-content-xl-stretch { - align-content: stretch !important; - } - .align-self-xl-auto { - align-self: auto !important; - } - .align-self-xl-start { - align-self: flex-start !important; - } - .align-self-xl-end { - align-self: flex-end !important; - } - .align-self-xl-center { - align-self: center !important; - } - .align-self-xl-baseline { - align-self: baseline !important; - } - .align-self-xl-stretch { - align-self: stretch !important; - } - .order-xl-first { - order: -1 !important; - } - .order-xl-0 { - order: 0 !important; - } - .order-xl-1 { - order: 1 !important; - } - .order-xl-2 { - order: 2 !important; - } - .order-xl-3 { - order: 3 !important; - } - .order-xl-4 { - order: 4 !important; - } - .order-xl-5 { - order: 5 !important; - } - .order-xl-last { - order: 6 !important; - } - .m-xl-0 { - margin: 0 !important; - } - .m-xl-1 { - margin: 0.25rem !important; - } - .m-xl-2 { - margin: 0.5rem !important; - } - .m-xl-3 { - margin: 1rem !important; - } - .m-xl-4 { - margin: 1.5rem !important; - } - .m-xl-5 { - margin: 3rem !important; - } - .m-xl-auto { - margin: auto !important; - } - .mx-xl-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - .mx-xl-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - .mx-xl-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - .mx-xl-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - .mx-xl-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - .mx-xl-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - .mx-xl-auto { - margin-right: auto !important; - margin-left: auto !important; - } - .my-xl-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - .my-xl-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - .my-xl-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - .my-xl-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - .my-xl-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - .my-xl-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - .my-xl-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - .mt-xl-0 { - margin-top: 0 !important; - } - .mt-xl-1 { - margin-top: 0.25rem !important; - } - .mt-xl-2 { - margin-top: 0.5rem !important; - } - .mt-xl-3 { - margin-top: 1rem !important; - } - .mt-xl-4 { - margin-top: 1.5rem !important; - } - .mt-xl-5 { - margin-top: 3rem !important; - } - .mt-xl-auto { - margin-top: auto !important; - } - .me-xl-0 { - margin-right: 0 !important; - } - .me-xl-1 { - margin-right: 0.25rem !important; - } - .me-xl-2 { - margin-right: 0.5rem !important; - } - .me-xl-3 { - margin-right: 1rem !important; - } - .me-xl-4 { - margin-right: 1.5rem !important; - } - .me-xl-5 { - margin-right: 3rem !important; - } - .me-xl-auto { - margin-right: auto !important; - } - .mb-xl-0 { - margin-bottom: 0 !important; - } - .mb-xl-1 { - margin-bottom: 0.25rem !important; - } - .mb-xl-2 { - margin-bottom: 0.5rem !important; - } - .mb-xl-3 { - margin-bottom: 1rem !important; - } - .mb-xl-4 { - margin-bottom: 1.5rem !important; - } - .mb-xl-5 { - margin-bottom: 3rem !important; - } - .mb-xl-auto { - margin-bottom: auto !important; - } - .ms-xl-0 { - margin-left: 0 !important; - } - .ms-xl-1 { - margin-left: 0.25rem !important; - } - .ms-xl-2 { - margin-left: 0.5rem !important; - } - .ms-xl-3 { - margin-left: 1rem !important; - } - .ms-xl-4 { - margin-left: 1.5rem !important; - } - .ms-xl-5 { - margin-left: 3rem !important; - } - .ms-xl-auto { - margin-left: auto !important; - } - .p-xl-0 { - padding: 0 !important; - } - .p-xl-1 { - padding: 0.25rem !important; - } - .p-xl-2 { - padding: 0.5rem !important; - } - .p-xl-3 { - padding: 1rem !important; - } - .p-xl-4 { - padding: 1.5rem !important; - } - .p-xl-5 { - padding: 3rem !important; - } - .px-xl-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - .px-xl-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - .px-xl-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - .px-xl-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - .px-xl-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - .px-xl-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - .py-xl-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - .py-xl-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - .py-xl-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - .py-xl-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - .py-xl-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - .py-xl-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - .pt-xl-0 { - padding-top: 0 !important; - } - .pt-xl-1 { - padding-top: 0.25rem !important; - } - .pt-xl-2 { - padding-top: 0.5rem !important; - } - .pt-xl-3 { - padding-top: 1rem !important; - } - .pt-xl-4 { - padding-top: 1.5rem !important; - } - .pt-xl-5 { - padding-top: 3rem !important; - } - .pe-xl-0 { - padding-right: 0 !important; - } - .pe-xl-1 { - padding-right: 0.25rem !important; - } - .pe-xl-2 { - padding-right: 0.5rem !important; - } - .pe-xl-3 { - padding-right: 1rem !important; - } - .pe-xl-4 { - padding-right: 1.5rem !important; - } - .pe-xl-5 { - padding-right: 3rem !important; - } - .pb-xl-0 { - padding-bottom: 0 !important; - } - .pb-xl-1 { - padding-bottom: 0.25rem !important; - } - .pb-xl-2 { - padding-bottom: 0.5rem !important; - } - .pb-xl-3 { - padding-bottom: 1rem !important; - } - .pb-xl-4 { - padding-bottom: 1.5rem !important; - } - .pb-xl-5 { - padding-bottom: 3rem !important; - } - .ps-xl-0 { - padding-left: 0 !important; - } - .ps-xl-1 { - padding-left: 0.25rem !important; - } - .ps-xl-2 { - padding-left: 0.5rem !important; - } - .ps-xl-3 { - padding-left: 1rem !important; - } - .ps-xl-4 { - padding-left: 1.5rem !important; - } - .ps-xl-5 { - padding-left: 3rem !important; - } - .gap-xl-0 { - gap: 0 !important; - } - .gap-xl-1 { - gap: 0.25rem !important; - } - .gap-xl-2 { - gap: 0.5rem !important; - } - .gap-xl-3 { - gap: 1rem !important; - } - .gap-xl-4 { - gap: 1.5rem !important; - } - .gap-xl-5 { - gap: 3rem !important; - } - .row-gap-xl-0 { - row-gap: 0 !important; - } - .row-gap-xl-1 { - row-gap: 0.25rem !important; - } - .row-gap-xl-2 { - row-gap: 0.5rem !important; - } - .row-gap-xl-3 { - row-gap: 1rem !important; - } - .row-gap-xl-4 { - row-gap: 1.5rem !important; - } - .row-gap-xl-5 { - row-gap: 3rem !important; - } - .column-gap-xl-0 { - -moz-column-gap: 0 !important; - column-gap: 0 !important; - } - .column-gap-xl-1 { - -moz-column-gap: 0.25rem !important; - column-gap: 0.25rem !important; - } - .column-gap-xl-2 { - -moz-column-gap: 0.5rem !important; - column-gap: 0.5rem !important; - } - .column-gap-xl-3 { - -moz-column-gap: 1rem !important; - column-gap: 1rem !important; - } - .column-gap-xl-4 { - -moz-column-gap: 1.5rem !important; - column-gap: 1.5rem !important; - } - .column-gap-xl-5 { - -moz-column-gap: 3rem !important; - column-gap: 3rem !important; - } - .text-xl-start { - text-align: left !important; - } - .text-xl-end { - text-align: right !important; - } - .text-xl-center { - text-align: center !important; - } -} -@media (min-width: 1400px) { - .float-xxl-start { - float: left !important; - } - .float-xxl-end { - float: right !important; - } - .float-xxl-none { - float: none !important; - } - .object-fit-xxl-contain { - -o-object-fit: contain !important; - object-fit: contain !important; - } - .object-fit-xxl-cover { - -o-object-fit: cover !important; - object-fit: cover !important; - } - .object-fit-xxl-fill { - -o-object-fit: fill !important; - object-fit: fill !important; - } - .object-fit-xxl-scale { - -o-object-fit: scale-down !important; - object-fit: scale-down !important; - } - .object-fit-xxl-none { - -o-object-fit: none !important; - object-fit: none !important; - } - .d-xxl-inline { - display: inline !important; - } - .d-xxl-inline-block { - display: inline-block !important; - } - .d-xxl-block { - display: block !important; - } - .d-xxl-grid { - display: grid !important; - } - .d-xxl-inline-grid { - display: inline-grid !important; - } - .d-xxl-table { - display: table !important; - } - .d-xxl-table-row { - display: table-row !important; - } - .d-xxl-table-cell { - display: table-cell !important; - } - .d-xxl-flex { - display: flex !important; - } - .d-xxl-inline-flex { - display: inline-flex !important; - } - .d-xxl-none { - display: none !important; - } - .flex-xxl-fill { - flex: 1 1 auto !important; - } - .flex-xxl-row { - flex-direction: row !important; - } - .flex-xxl-column { - flex-direction: column !important; - } - .flex-xxl-row-reverse { - flex-direction: row-reverse !important; - } - .flex-xxl-column-reverse { - flex-direction: column-reverse !important; - } - .flex-xxl-grow-0 { - flex-grow: 0 !important; - } - .flex-xxl-grow-1 { - flex-grow: 1 !important; - } - .flex-xxl-shrink-0 { - flex-shrink: 0 !important; - } - .flex-xxl-shrink-1 { - flex-shrink: 1 !important; - } - .flex-xxl-wrap { - flex-wrap: wrap !important; - } - .flex-xxl-nowrap { - flex-wrap: nowrap !important; - } - .flex-xxl-wrap-reverse { - flex-wrap: wrap-reverse !important; - } - .justify-content-xxl-start { - justify-content: flex-start !important; - } - .justify-content-xxl-end { - justify-content: flex-end !important; - } - .justify-content-xxl-center { - justify-content: center !important; - } - .justify-content-xxl-between { - justify-content: space-between !important; - } - .justify-content-xxl-around { - justify-content: space-around !important; - } - .justify-content-xxl-evenly { - justify-content: space-evenly !important; - } - .align-items-xxl-start { - align-items: flex-start !important; - } - .align-items-xxl-end { - align-items: flex-end !important; - } - .align-items-xxl-center { - align-items: center !important; - } - .align-items-xxl-baseline { - align-items: baseline !important; - } - .align-items-xxl-stretch { - align-items: stretch !important; - } - .align-content-xxl-start { - align-content: flex-start !important; - } - .align-content-xxl-end { - align-content: flex-end !important; - } - .align-content-xxl-center { - align-content: center !important; - } - .align-content-xxl-between { - align-content: space-between !important; - } - .align-content-xxl-around { - align-content: space-around !important; - } - .align-content-xxl-stretch { - align-content: stretch !important; - } - .align-self-xxl-auto { - align-self: auto !important; - } - .align-self-xxl-start { - align-self: flex-start !important; - } - .align-self-xxl-end { - align-self: flex-end !important; - } - .align-self-xxl-center { - align-self: center !important; - } - .align-self-xxl-baseline { - align-self: baseline !important; - } - .align-self-xxl-stretch { - align-self: stretch !important; - } - .order-xxl-first { - order: -1 !important; - } - .order-xxl-0 { - order: 0 !important; - } - .order-xxl-1 { - order: 1 !important; - } - .order-xxl-2 { - order: 2 !important; - } - .order-xxl-3 { - order: 3 !important; - } - .order-xxl-4 { - order: 4 !important; - } - .order-xxl-5 { - order: 5 !important; - } - .order-xxl-last { - order: 6 !important; - } - .m-xxl-0 { - margin: 0 !important; - } - .m-xxl-1 { - margin: 0.25rem !important; - } - .m-xxl-2 { - margin: 0.5rem !important; - } - .m-xxl-3 { - margin: 1rem !important; - } - .m-xxl-4 { - margin: 1.5rem !important; - } - .m-xxl-5 { - margin: 3rem !important; - } - .m-xxl-auto { - margin: auto !important; - } - .mx-xxl-0 { - margin-right: 0 !important; - margin-left: 0 !important; - } - .mx-xxl-1 { - margin-right: 0.25rem !important; - margin-left: 0.25rem !important; - } - .mx-xxl-2 { - margin-right: 0.5rem !important; - margin-left: 0.5rem !important; - } - .mx-xxl-3 { - margin-right: 1rem !important; - margin-left: 1rem !important; - } - .mx-xxl-4 { - margin-right: 1.5rem !important; - margin-left: 1.5rem !important; - } - .mx-xxl-5 { - margin-right: 3rem !important; - margin-left: 3rem !important; - } - .mx-xxl-auto { - margin-right: auto !important; - margin-left: auto !important; - } - .my-xxl-0 { - margin-top: 0 !important; - margin-bottom: 0 !important; - } - .my-xxl-1 { - margin-top: 0.25rem !important; - margin-bottom: 0.25rem !important; - } - .my-xxl-2 { - margin-top: 0.5rem !important; - margin-bottom: 0.5rem !important; - } - .my-xxl-3 { - margin-top: 1rem !important; - margin-bottom: 1rem !important; - } - .my-xxl-4 { - margin-top: 1.5rem !important; - margin-bottom: 1.5rem !important; - } - .my-xxl-5 { - margin-top: 3rem !important; - margin-bottom: 3rem !important; - } - .my-xxl-auto { - margin-top: auto !important; - margin-bottom: auto !important; - } - .mt-xxl-0 { - margin-top: 0 !important; - } - .mt-xxl-1 { - margin-top: 0.25rem !important; - } - .mt-xxl-2 { - margin-top: 0.5rem !important; - } - .mt-xxl-3 { - margin-top: 1rem !important; - } - .mt-xxl-4 { - margin-top: 1.5rem !important; - } - .mt-xxl-5 { - margin-top: 3rem !important; - } - .mt-xxl-auto { - margin-top: auto !important; - } - .me-xxl-0 { - margin-right: 0 !important; - } - .me-xxl-1 { - margin-right: 0.25rem !important; - } - .me-xxl-2 { - margin-right: 0.5rem !important; - } - .me-xxl-3 { - margin-right: 1rem !important; - } - .me-xxl-4 { - margin-right: 1.5rem !important; - } - .me-xxl-5 { - margin-right: 3rem !important; - } - .me-xxl-auto { - margin-right: auto !important; - } - .mb-xxl-0 { - margin-bottom: 0 !important; - } - .mb-xxl-1 { - margin-bottom: 0.25rem !important; - } - .mb-xxl-2 { - margin-bottom: 0.5rem !important; - } - .mb-xxl-3 { - margin-bottom: 1rem !important; - } - .mb-xxl-4 { - margin-bottom: 1.5rem !important; - } - .mb-xxl-5 { - margin-bottom: 3rem !important; - } - .mb-xxl-auto { - margin-bottom: auto !important; - } - .ms-xxl-0 { - margin-left: 0 !important; - } - .ms-xxl-1 { - margin-left: 0.25rem !important; - } - .ms-xxl-2 { - margin-left: 0.5rem !important; - } - .ms-xxl-3 { - margin-left: 1rem !important; - } - .ms-xxl-4 { - margin-left: 1.5rem !important; - } - .ms-xxl-5 { - margin-left: 3rem !important; - } - .ms-xxl-auto { - margin-left: auto !important; - } - .p-xxl-0 { - padding: 0 !important; - } - .p-xxl-1 { - padding: 0.25rem !important; - } - .p-xxl-2 { - padding: 0.5rem !important; - } - .p-xxl-3 { - padding: 1rem !important; - } - .p-xxl-4 { - padding: 1.5rem !important; - } - .p-xxl-5 { - padding: 3rem !important; - } - .px-xxl-0 { - padding-right: 0 !important; - padding-left: 0 !important; - } - .px-xxl-1 { - padding-right: 0.25rem !important; - padding-left: 0.25rem !important; - } - .px-xxl-2 { - padding-right: 0.5rem !important; - padding-left: 0.5rem !important; - } - .px-xxl-3 { - padding-right: 1rem !important; - padding-left: 1rem !important; - } - .px-xxl-4 { - padding-right: 1.5rem !important; - padding-left: 1.5rem !important; - } - .px-xxl-5 { - padding-right: 3rem !important; - padding-left: 3rem !important; - } - .py-xxl-0 { - padding-top: 0 !important; - padding-bottom: 0 !important; - } - .py-xxl-1 { - padding-top: 0.25rem !important; - padding-bottom: 0.25rem !important; - } - .py-xxl-2 { - padding-top: 0.5rem !important; - padding-bottom: 0.5rem !important; - } - .py-xxl-3 { - padding-top: 1rem !important; - padding-bottom: 1rem !important; - } - .py-xxl-4 { - padding-top: 1.5rem !important; - padding-bottom: 1.5rem !important; - } - .py-xxl-5 { - padding-top: 3rem !important; - padding-bottom: 3rem !important; - } - .pt-xxl-0 { - padding-top: 0 !important; - } - .pt-xxl-1 { - padding-top: 0.25rem !important; - } - .pt-xxl-2 { - padding-top: 0.5rem !important; - } - .pt-xxl-3 { - padding-top: 1rem !important; - } - .pt-xxl-4 { - padding-top: 1.5rem !important; - } - .pt-xxl-5 { - padding-top: 3rem !important; - } - .pe-xxl-0 { - padding-right: 0 !important; - } - .pe-xxl-1 { - padding-right: 0.25rem !important; - } - .pe-xxl-2 { - padding-right: 0.5rem !important; - } - .pe-xxl-3 { - padding-right: 1rem !important; - } - .pe-xxl-4 { - padding-right: 1.5rem !important; - } - .pe-xxl-5 { - padding-right: 3rem !important; - } - .pb-xxl-0 { - padding-bottom: 0 !important; - } - .pb-xxl-1 { - padding-bottom: 0.25rem !important; - } - .pb-xxl-2 { - padding-bottom: 0.5rem !important; - } - .pb-xxl-3 { - padding-bottom: 1rem !important; - } - .pb-xxl-4 { - padding-bottom: 1.5rem !important; - } - .pb-xxl-5 { - padding-bottom: 3rem !important; - } - .ps-xxl-0 { - padding-left: 0 !important; - } - .ps-xxl-1 { - padding-left: 0.25rem !important; - } - .ps-xxl-2 { - padding-left: 0.5rem !important; - } - .ps-xxl-3 { - padding-left: 1rem !important; - } - .ps-xxl-4 { - padding-left: 1.5rem !important; - } - .ps-xxl-5 { - padding-left: 3rem !important; - } - .gap-xxl-0 { - gap: 0 !important; - } - .gap-xxl-1 { - gap: 0.25rem !important; - } - .gap-xxl-2 { - gap: 0.5rem !important; - } - .gap-xxl-3 { - gap: 1rem !important; - } - .gap-xxl-4 { - gap: 1.5rem !important; - } - .gap-xxl-5 { - gap: 3rem !important; - } - .row-gap-xxl-0 { - row-gap: 0 !important; - } - .row-gap-xxl-1 { - row-gap: 0.25rem !important; - } - .row-gap-xxl-2 { - row-gap: 0.5rem !important; - } - .row-gap-xxl-3 { - row-gap: 1rem !important; - } - .row-gap-xxl-4 { - row-gap: 1.5rem !important; - } - .row-gap-xxl-5 { - row-gap: 3rem !important; - } - .column-gap-xxl-0 { - -moz-column-gap: 0 !important; - column-gap: 0 !important; - } - .column-gap-xxl-1 { - -moz-column-gap: 0.25rem !important; - column-gap: 0.25rem !important; - } - .column-gap-xxl-2 { - -moz-column-gap: 0.5rem !important; - column-gap: 0.5rem !important; - } - .column-gap-xxl-3 { - -moz-column-gap: 1rem !important; - column-gap: 1rem !important; - } - .column-gap-xxl-4 { - -moz-column-gap: 1.5rem !important; - column-gap: 1.5rem !important; - } - .column-gap-xxl-5 { - -moz-column-gap: 3rem !important; - column-gap: 3rem !important; - } - .text-xxl-start { - text-align: left !important; - } - .text-xxl-end { - text-align: right !important; - } - .text-xxl-center { - text-align: center !important; - } -} -@media (min-width: 1200px) { - .fs-1 { - font-size: 4rem !important; - } - .fs-2 { - font-size: 3rem !important; - } - .fs-3 { - font-size: 2.5rem !important; - } - .fs-4 { - font-size: 2rem !important; - } - .fs-5 { - font-size: 1.5rem !important; - } -} -@media print { - .d-print-inline { - display: inline !important; - } - .d-print-inline-block { - display: inline-block !important; - } - .d-print-block { - display: block !important; - } - .d-print-grid { - display: grid !important; - } - .d-print-inline-grid { - display: inline-grid !important; - } - .d-print-table { - display: table !important; - } - .d-print-table-row { - display: table-row !important; - } - .d-print-table-cell { - display: table-cell !important; - } - .d-print-flex { - display: flex !important; - } - .d-print-inline-flex { - display: inline-flex !important; - } - .d-print-none { - display: none !important; - } -} -.navbar.bg-primary { - border: 1px solid #282828; -} -.navbar.bg-dark { - background-color: #060606 !important; - border: 1px solid #282828; -} -.navbar.bg-light { - background-color: #888 !important; -} -.navbar.fixed-top { - border-width: 0 0 1px; -} -.navbar.fixed-bottom { - border-width: 1px 0 0; -} - -.btn-primary { - background-color: #2a9fd6; -} -.btn-secondary { - background-color: #555; -} -.btn-success { - background-color: #77b300; -} -.btn-info { - background-color: #93c; -} -.btn-warning { - background-color: #f80; -} -.btn-danger { - background-color: #c00; -} -.btn-light { - background-color: #222; -} -.btn-dark { - background-color: #adafae; -} - -legend { - color: #fff; -} - -.form-control:disabled, .form-control[readonly] { - border-color: transparent; -} - -.nav-tabs .nav-link, -.nav-pills .nav-link { - color: #fff; -} -.nav-tabs .nav-link:hover, -.nav-pills .nav-link:hover { - background-color: #282828; -} -.nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, -.nav-pills .nav-link.disabled, -.nav-pills .nav-link.disabled:hover { - color: var(--bs-secondary-color); - background-color: transparent; -} -.nav-tabs .nav-link.active, -.nav-pills .nav-link.active { - background-color: #2a9fd6; -} - -.breadcrumb a { - color: #fff; -} - -.pagination a:hover { - text-decoration: none; -} - -.alert { - color: #fff; - border: none; -} -.alert a, -.alert .alert-link { - color: #fff; - text-decoration: underline; -} -.alert-primary { - background-color: #2a9fd6; -} -.alert-secondary { - background-color: #555; -} -.alert-success { - background-color: #77b300; -} -.alert-info { - background-color: #93c; -} -.alert-warning { - background-color: #f80; -} -.alert-danger { - background-color: #c00; -} -.alert-light { - background-color: #222; -} -.alert-dark { - background-color: #adafae; -} - -.badge.bg-dark { - color: #212529; -} - -.tooltip { - --bs-tooltip-bg: var(--bs-tertiary-bg); - --bs-tooltip-color: var(--bs-emphasis-color); -} - -.list-group-item-action:hover { - border-color: #2a9fd6; -} - -.popover-title { - border-bottom: none; -} \ No newline at end of file diff --git a/assets/script.js b/assets/script.js deleted file mode 100644 index f987c2c..0000000 --- a/assets/script.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Delete a client referenced by its ID - * - * @param clientID The ID of the client to delete - */ -async function deleteClient(clientID) { - if(!confirm("Do you really want to remove client " + clientID + "? The operation cannot be reverted!")) - return; - - try { - const res = await fetch("/", { - method: "POST", - headers:{ - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: new URLSearchParams({ - "delete_client_id": clientID - }), - }); - - if(res.status !== 200) - throw new Error(`Invalid status code: ${res.status}`); - - alert("The client was successfully deleted!"); - location.reload(); - } catch (e) { - console.error(`Failed to delete client: ${e}`); - alert("Failed to delete client!"); - } -} \ No newline at end of file diff --git a/assets/style.css b/assets/style.css deleted file mode 100644 index f83653e..0000000 --- a/assets/style.css +++ /dev/null @@ -1,12 +0,0 @@ -.body-content { - max-width: 900px; - margin: 50px auto; -} - -.body-content .card-header { - font-weight: bold; -} - -#user_id_container { - margin: 20px auto; -} \ No newline at end of file diff --git a/assets/ws_debug.js b/assets/ws_debug.js deleted file mode 100644 index b001790..0000000 --- a/assets/ws_debug.js +++ /dev/null @@ -1,68 +0,0 @@ -let ws; - -const JS_MESSAGE = "JS code"; -const IN_MESSAGE = "Incoming"; - -/** - * Log message - */ -function log(src, txt) { - const target = document.getElementById("ws_log"); - const msg = document.createElement("div"); - msg.className = "message"; - msg.innerHTML = `
${src}
${txt}
` - target.insertBefore(msg, target.firstChild); -} - -/** - * Set the state of the WebSocket - */ -function setState(state) { - document.getElementById("state").innerText = state; -} - -/** - * Initialize WebSocket connection - */ -function connect() { - disconnect(); - log(JS_MESSAGE, "Initialize connection..."); - ws = new WebSocket("/api/ws"); - setState("Connecting..."); - ws.onopen = function () { - log(JS_MESSAGE, "Connected to WebSocket !"); - setState("Connected"); - } - ws.onmessage = function (event) { - log(IN_MESSAGE, event.data); - } - ws.onclose = function () { - log(JS_MESSAGE, "Disconnected from WebSocket !"); - setState("Disconnected"); - } - ws.onerror = function (event) { - console.error("WS Error!", event); - log(JS_MESSAGE, `Error with websocket! ${event}`); - setState("Error"); - } -} - -/** - * Close WebSocket connection - */ -function disconnect() { - if (ws && ws.readyState === WebSocket.OPEN) { - log(JS_MESSAGE, "Close connection..."); - ws.close(); - } - - setState("Disconnected"); - ws = undefined; -} - -/** - * Clear WS logs - */ -function clearLogs() { - document.getElementById("ws_log").innerHTML = ""; -} \ No newline at end of file diff --git a/docker/dex/dex.config.yaml b/docker/dex/dex.config.yaml deleted file mode 100644 index 7704800..0000000 --- a/docker/dex/dex.config.yaml +++ /dev/null @@ -1,26 +0,0 @@ -issuer: http://127.0.0.1:9001/dex - -storage: - type: memory - -web: - http: 0.0.0.0:9001 - -oauth2: - # Automate some clicking - # Note: this might actually make some tests pass that otherwise wouldn't. - skipApprovalScreen: false - -connectors: - # Note: this might actually make some tests pass that otherwise wouldn't. - - type: mockCallback - id: mock - name: Example - -# Basic OP test suite requires two clients. -staticClients: - - id: foo - secret: bar - redirectURIs: - - http://localhost:8000/oidc_cb - name: Project diff --git a/.gitignore b/matrixgw_backend/.gitignore similarity index 100% rename from .gitignore rename to matrixgw_backend/.gitignore diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml new file mode 100644 index 0000000..ab0c915 --- /dev/null +++ b/matrixgw_backend/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "matrixgw_backend" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/Dockerfile b/matrixgw_backend/Dockerfile similarity index 100% rename from Dockerfile rename to matrixgw_backend/Dockerfile diff --git a/docker-compose.yml b/matrixgw_backend/docker-compose.yml similarity index 95% rename from docker-compose.yml rename to matrixgw_backend/docker-compose.yml index d37a458..26d2b1d 100644 --- a/docker-compose.yml +++ b/matrixgw_backend/docker-compose.yml @@ -84,14 +84,6 @@ services: volumes: - ./docker/element/config.json:/app/config.json:ro - oidc: - image: dexidp/dex - ports: - - 9001:9001 - volumes: - - ./docker/dex:/conf:ro - command: ["dex", "serve", "/conf/dex.config.yaml"] - minio: image: quay.io/minio/minio command: minio server --console-address ":9002" /data diff --git a/docker/element/config.json b/matrixgw_backend/docker/element/config.json similarity index 100% rename from docker/element/config.json rename to matrixgw_backend/docker/element/config.json diff --git a/docker/mas/config.yaml b/matrixgw_backend/docker/mas/config.yaml similarity index 100% rename from docker/mas/config.yaml rename to matrixgw_backend/docker/mas/config.yaml diff --git a/docker/synapse/homeserver.yaml b/matrixgw_backend/docker/synapse/homeserver.yaml similarity index 100% rename from docker/synapse/homeserver.yaml rename to matrixgw_backend/docker/synapse/homeserver.yaml diff --git a/docker/synapse/localhost.log.config b/matrixgw_backend/docker/synapse/localhost.log.config similarity index 100% rename from docker/synapse/localhost.log.config rename to matrixgw_backend/docker/synapse/localhost.log.config diff --git a/docker/synapse/localhost.signing.key b/matrixgw_backend/docker/synapse/localhost.signing.key similarity index 100% rename from docker/synapse/localhost.signing.key rename to matrixgw_backend/docker/synapse/localhost.signing.key diff --git a/examples/api_curl.rs b/matrixgw_backend/examples/api_curl.rs similarity index 100% rename from examples/api_curl.rs rename to matrixgw_backend/examples/api_curl.rs diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/matrixgw_backend/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/matrixgw_frontend/.gitignore b/matrixgw_frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/matrixgw_frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/matrixgw_frontend/README.md b/matrixgw_frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/matrixgw_frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/matrixgw_frontend/eslint.config.js b/matrixgw_frontend/eslint.config.js new file mode 100644 index 0000000..b19330b --- /dev/null +++ b/matrixgw_frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/assets/favicon.png b/matrixgw_frontend/favicon.png similarity index 100% rename from assets/favicon.png rename to matrixgw_frontend/favicon.png diff --git a/matrixgw_frontend/index.html b/matrixgw_frontend/index.html new file mode 100644 index 0000000..03c4e8f --- /dev/null +++ b/matrixgw_frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + matrixgw_frontend + + +
+ + + diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json new file mode 100644 index 0000000..612a943 --- /dev/null +++ b/matrixgw_frontend/package-lock.json @@ -0,0 +1,3265 @@ +{ + "name": "matrixgw_frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "matrixgw_frontend", + "version": "0.0.0", + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "npm:rolldown-vite@7.1.14" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", + "integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", + "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.92.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.92.0.tgz", + "integrity": "sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.93.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.93.0.tgz", + "integrity": "sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.41.tgz", + "integrity": "sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.41.tgz", + "integrity": "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.41.tgz", + "integrity": "sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.41.tgz", + "integrity": "sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.41.tgz", + "integrity": "sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.41.tgz", + "integrity": "sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.41.tgz", + "integrity": "sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.41.tgz", + "integrity": "sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.41.tgz", + "integrity": "sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.41.tgz", + "integrity": "sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.41.tgz", + "integrity": "sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.0.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.41.tgz", + "integrity": "sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-ia32-msvc": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.41.tgz", + "integrity": "sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.41.tgz", + "integrity": "sha512-UlpxKmFdik0Y2VjZrgUCgoYArZJiZllXgIipdBRV1hw6uK45UbQabSTW6Kp6enuOu7vouYWftwhuxfpE8J2JAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.43", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", + "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", + "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.43", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.22.tgz", + "integrity": "sha512-/tk9kky/d8T8CTXIQYASLyhAxR5VwL3zct1oAoVTaOUHwrmsGnfbRwNdEq+vOl2BN8i3PcDdP0o4Q+jjKQoFbQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001752", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001752.tgz", + "integrity": "sha512-vKUk7beoukxE47P5gcVNKkDRzXdVofotshHwfR9vmpeFKxmI5PBpgOMC18LUJUA/DvJ70Y7RveasIBraqsyO/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.244", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.41.tgz", + "integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.93.0", + "@rolldown/pluginutils": "1.0.0-beta.41", + "ansis": "=4.2.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.41", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.41", + "@rolldown/binding-darwin-x64": "1.0.0-beta.41", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.41", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.41", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.41", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.41", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.41", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.41", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.41", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.41", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.41", + "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.41", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.41" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.41", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.41.tgz", + "integrity": "sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", + "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "name": "rolldown-vite", + "version": "7.1.14", + "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.1.14.tgz", + "integrity": "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.92.0", + "fdir": "^6.5.0", + "lightningcss": "^1.30.1", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rolldown": "1.0.0-beta.41", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "esbuild": "^0.25.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json new file mode 100644 index 0000000..33d4639 --- /dev/null +++ b/matrixgw_frontend/package.json @@ -0,0 +1,33 @@ +{ + "name": "matrixgw_frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "npm:rolldown-vite@7.1.14" + }, + "overrides": { + "vite": "npm:rolldown-vite@7.1.14" + } +} diff --git a/matrixgw_frontend/public/vite.svg b/matrixgw_frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/matrixgw_frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/matrixgw_frontend/src/App.css b/matrixgw_frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/matrixgw_frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx new file mode 100644 index 0000000..3d7ded3 --- /dev/null +++ b/matrixgw_frontend/src/App.tsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ) +} + +export default App diff --git a/matrixgw_frontend/src/assets/react.svg b/matrixgw_frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/matrixgw_frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/matrixgw_frontend/src/index.css b/matrixgw_frontend/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/matrixgw_frontend/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/matrixgw_frontend/src/main.tsx b/matrixgw_frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/matrixgw_frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/matrixgw_frontend/tsconfig.app.json b/matrixgw_frontend/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/matrixgw_frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/matrixgw_frontend/tsconfig.json b/matrixgw_frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/matrixgw_frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/matrixgw_frontend/tsconfig.node.json b/matrixgw_frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/matrixgw_frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/matrixgw_frontend/vite.config.ts b/matrixgw_frontend/vite.config.ts new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/matrixgw_frontend/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/src/app_config.rs b/src/app_config.rs deleted file mode 100644 index 014effe..0000000 --- a/src/app_config.rs +++ /dev/null @@ -1,187 +0,0 @@ -use clap::Parser; -use s3::creds::Credentials; -use s3::{Bucket, Region}; - -/// Matrix gateway backend API -#[derive(Parser, Debug, Clone)] -#[clap(author, version, about, long_about = None)] -pub struct AppConfig { - /// Listen address - #[clap(short, long, env, default_value = "0.0.0.0:8000")] - pub listen_address: String, - - /// Website origin - #[clap(short, long, env, default_value = "http://localhost:8000")] - pub website_origin: String, - - /// Proxy IP, might end with a star "*" - #[clap(short, long, env)] - pub proxy_ip: Option, - - /// Secret key, used to sign some resources. Must be randomly generated - #[clap(short = 'S', long, env, default_value = "")] - secret: String, - - /// Matrix API origin - #[clap(short, long, env, default_value = "http://127.0.0.1:8448")] - pub matrix_homeserver: String, - - /// Redis connection hostname - #[clap(long, env, default_value = "localhost")] - redis_hostname: String, - - /// Redis connection port - #[clap(long, env, default_value_t = 6379)] - redis_port: u16, - - /// Redis database number - #[clap(long, env, default_value_t = 0)] - redis_db_number: i64, - - /// Redis username - #[clap(long, env)] - redis_username: Option, - - /// Redis password - #[clap(long, env, default_value = "secretredis")] - redis_password: String, - - /// URL where the OpenID configuration can be found - #[arg( - long, - env, - default_value = "http://localhost:9001/dex/.well-known/openid-configuration" - )] - pub oidc_configuration_url: String, - - /// OpenID client ID - #[arg(long, env, default_value = "foo")] - pub oidc_client_id: String, - - /// OpenID client secret - #[arg(long, env, default_value = "bar")] - pub oidc_client_secret: String, - - /// OpenID login redirect URL - #[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")] - oidc_redirect_url: String, - - /// S3 Bucket name - #[arg(long, env, default_value = "matrix-gw")] - s3_bucket_name: String, - - /// S3 region (if not using Minio) - #[arg(long, env, default_value = "eu-central-1")] - s3_region: String, - - /// S3 API endpoint - #[arg(long, env, default_value = "http://localhost:9000")] - s3_endpoint: String, - - /// S3 access key - #[arg(long, env, default_value = "minioadmin")] - s3_access_key: String, - - /// S3 secret key - #[arg(long, env, default_value = "minioadmin")] - s3_secret_key: String, - - /// S3 skip auto create bucket if not existing - #[arg(long, env)] - pub s3_skip_auto_create_bucket: bool, -} - -lazy_static::lazy_static! { - static ref ARGS: AppConfig = { - AppConfig::parse() - }; -} - -impl AppConfig { - /// Get parsed command line arguments - pub fn get() -> &'static AppConfig { - &ARGS - } - - /// Get app secret - pub fn secret(&self) -> &str { - let mut secret = self.secret.as_str(); - - if cfg!(debug_assertions) && secret.is_empty() { - secret = "DEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEY"; - } - - if secret.is_empty() { - panic!("SECRET is undefined or too short (min 64 chars)!") - } - - secret - } - - /// Get Redis connection configuration - pub fn redis_connection_string(&self) -> String { - format!( - "redis://{}:{}@{}:{}/{}", - self.redis_username.as_deref().unwrap_or(""), - self.redis_password, - self.redis_hostname, - self.redis_port, - self.redis_db_number - ) - } - - /// Get OpenID providers configuration - pub fn openid_provider(&self) -> OIDCProvider<'_> { - OIDCProvider { - client_id: self.oidc_client_id.as_str(), - client_secret: self.oidc_client_secret.as_str(), - configuration_url: self.oidc_configuration_url.as_str(), - redirect_url: self - .oidc_redirect_url - .replace("APP_ORIGIN", &self.website_origin), - } - } - - /// Get s3 bucket credentials - pub fn s3_credentials(&self) -> anyhow::Result { - Ok(Credentials::new( - Some(&self.s3_access_key), - Some(&self.s3_secret_key), - None, - None, - None, - )?) - } - - /// Get S3 bucket - pub fn s3_bucket(&self) -> anyhow::Result> { - Ok(Bucket::new( - &self.s3_bucket_name, - Region::Custom { - region: self.s3_region.to_string(), - endpoint: self.s3_endpoint.to_string(), - }, - self.s3_credentials()?, - )? - .with_path_style()) - } -} - -#[derive(Debug, Clone, serde::Serialize)] -pub struct OIDCProvider<'a> { - pub client_id: &'a str, - pub client_secret: &'a str, - pub configuration_url: &'a str, - pub redirect_url: String, -} - -#[cfg(test)] -mod test { - use crate::app_config::AppConfig; - - #[test] - fn verify_cli() { - use clap::CommandFactory; - AppConfig::command().debug_assert() - } -} diff --git a/src/broadcast_messages.rs b/src/broadcast_messages.rs deleted file mode 100644 index 4a77381..0000000 --- a/src/broadcast_messages.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::sync_client::SyncClientID; -use crate::user::{APIClientID, UserID}; -use ruma::api::client::sync::sync_events::v3::{GlobalAccountData, Presence, Rooms, ToDevice}; -use ruma::api::client::sync::sync_events::DeviceLists; - -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] -pub struct SyncEvent { - /// Updates to rooms. - #[serde(default, skip_serializing_if = "Rooms::is_empty")] - pub rooms: Rooms, - - /// Updates to the presence status of other users. - #[serde(default, skip_serializing_if = "Presence::is_empty")] - pub presence: Presence, - - /// The global private data created by this user. - #[serde(default, skip_serializing_if = "GlobalAccountData::is_empty")] - pub account_data: GlobalAccountData, - - /// Messages sent directly between devices. - #[serde(default, skip_serializing_if = "ToDevice::is_empty")] - pub to_device: ToDevice, - - /// Information on E2E device updates. - /// - /// Only present on an incremental sync. - #[serde(default, skip_serializing_if = "DeviceLists::is_empty")] - pub device_lists: DeviceLists, -} - -/// Broadcast messages -#[derive(Debug, Clone)] -pub enum BroadcastMessage { - /// Request to close the session of a specific client - CloseClientSession(APIClientID), - /// Close all the sessions of a given user - CloseAllUserSessions(UserID), - /// Stop sync client for a given user - StopSyncTaskForUser(UserID), - /// Start sync client for a given user (if not already running) - StartSyncTaskForUser(UserID), - /// Stop a client with a given client ID - StopSyncClient(SyncClientID), - /// Propagate a new sync event - SyncEvent(UserID, Box), -} diff --git a/src/constants.rs b/src/constants.rs deleted file mode 100644 index 539f496..0000000 --- a/src/constants.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::time::Duration; - -/// Session key for OpenID login state -pub const STATE_KEY: &str = "oidc-state"; - -/// Session key for user information -pub const USER_SESSION_KEY: &str = "user"; - -/// Token length -pub const TOKEN_LEN: usize = 20; - -/// How often heartbeat pings are sent. -/// -/// Should be half (or less) of the acceptable client timeout. -pub const WS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); - -/// How long before lack of client response causes a timeout. -pub const WS_CLIENT_TIMEOUT: Duration = Duration::from_secs(10); diff --git a/src/extractors/client_auth.rs b/src/extractors/client_auth.rs deleted file mode 100644 index a7a4a5b..0000000 --- a/src/extractors/client_auth.rs +++ /dev/null @@ -1,256 +0,0 @@ -use crate::constants::USER_SESSION_KEY; -use crate::server::HttpFailure; -use crate::user::{APIClient, APIClientID, RumaClient, User, UserConfig, UserID}; -use crate::utils::base_utils::curr_time; -use actix_remote_ip::RemoteIP; -use actix_session::Session; -use actix_web::dev::Payload; -use actix_web::{FromRequest, HttpRequest}; -use bytes::Bytes; -use jwt_simple::common::VerificationOptions; -use jwt_simple::prelude::{Duration, HS256Key, MACLike}; -use ruma::api::{IncomingResponse, OutgoingRequest}; -use sha2::{Digest, Sha256}; -use std::net::IpAddr; -use std::str::FromStr; - -pub struct APIClientAuth { - pub user: UserConfig, - pub client: Option, - pub payload: Option>, -} - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct TokenClaims { - #[serde(rename = "met")] - pub method: String, - pub uri: String, - #[serde(rename = "pay", skip_serializing_if = "Option::is_none")] - pub payload_sha256: Option, -} - -impl APIClientAuth { - async fn extract_auth( - req: &HttpRequest, - remote_ip: IpAddr, - payload_bytes: Option, - ) -> Result { - // Check if user is authenticated using Web UI - let session = Session::from_request(req, &mut Payload::None).await?; - - if let Some(user) = session.get::(USER_SESSION_KEY)? { - match UserConfig::load(&user.id, false).await { - Ok(config) => { - return Ok(Self { - user: config, - client: None, - payload: payload_bytes.map(|bytes| bytes.to_vec()), - }) - } - Err(e) => { - log::error!("Failed to fetch user information for authentication using cookie token! {e}"); - } - }; - } - - let Some(token) = req.headers().get("x-client-auth") else { - return Err(actix_web::error::ErrorBadRequest( - "Missing authentication header!", - )); - }; - let Ok(jwt_token) = token.to_str() else { - return Err(actix_web::error::ErrorBadRequest( - "Failed to decode token as string!", - )); - }; - - let metadata = match jwt_simple::token::Token::decode_metadata(jwt_token) { - Ok(m) => m, - Err(e) => { - log::error!("Failed to decode JWT header metadata! {e}"); - return Err(actix_web::error::ErrorBadRequest( - "Failed to decode JWT header metadata!", - )); - } - }; - - let Some(kid) = metadata.key_id() else { - return Err(actix_web::error::ErrorBadRequest( - "Missing key id in request!", - )); - }; - - let Some((user_id, client_id)) = kid.split_once("#") else { - return Err(actix_web::error::ErrorBadRequest( - "Invalid key format (missing part)!", - )); - }; - - let (Ok(user_id), Ok(client_id)) = - (urlencoding::decode(user_id), urlencoding::decode(client_id)) - else { - return Err(actix_web::error::ErrorBadRequest( - "Invalid key format (decoding failed)!", - )); - }; - - // Fetch user - const USER_NOT_FOUND_ERROR: &str = "User not found!"; - let user = match UserConfig::load(&UserID(user_id.to_string()), false).await { - Ok(u) => u, - Err(e) => { - log::error!("Failed to get user information! {e}"); - return Err(actix_web::error::ErrorForbidden(USER_NOT_FOUND_ERROR)); - } - }; - - // Find client - let Ok(client_id) = APIClientID::from_str(&client_id) else { - return Err(actix_web::error::ErrorBadRequest("Invalid token format!")); - }; - let Some(client) = user.find_client_by_id(&client_id) else { - log::error!("Client not found for user!"); - return Err(actix_web::error::ErrorForbidden(USER_NOT_FOUND_ERROR)); - }; - - // Decode JWT - let key = HS256Key::from_bytes(client.secret.as_bytes()); - let verif = VerificationOptions { - max_validity: Some(Duration::from_mins(15)), - ..Default::default() - }; - - let claims = match key.verify_token::(jwt_token, Some(verif)) { - Ok(t) => t, - Err(e) => { - log::error!("JWT validation failed! {e}"); - return Err(actix_web::error::ErrorForbidden("JWT validation failed!")); - } - }; - - // Check for nonce - if claims.nonce.is_none() { - return Err(actix_web::error::ErrorBadRequest( - "A nonce is required in JWT!", - )); - } - - // Check IP restriction - if let Some(net) = client.network { - if !net.contains(&remote_ip) { - log::error!( - "Trying to use client {} from unauthorized IP address: {remote_ip}", - client.id.0 - ); - return Err(actix_web::error::ErrorForbidden( - "This client cannot be used from this IP address!", - )); - } - } - - // Check URI & verb - if claims.custom.uri != req.uri().to_string() { - return Err(actix_web::error::ErrorBadRequest("URI mismatch!")); - } - if claims.custom.method != req.method().to_string() { - return Err(actix_web::error::ErrorBadRequest("Method mismatch!")); - } - - // Check for write access - if client.readonly_client && !req.method().is_safe() { - return Err(actix_web::error::ErrorBadRequest( - "Read only client cannot perform write operations!", - )); - } - - let payload = match (payload_bytes, claims.custom.payload_sha256) { - (None, _) => None, - (Some(_), None) => { - return Err(actix_web::error::ErrorBadRequest( - "A payload digest must be included in the JWT when the request has a payload!", - )); - } - (Some(payload), Some(provided_digest)) => { - let computed_digest = base16ct::lower::encode_string(&Sha256::digest(&payload)); - if computed_digest != provided_digest { - log::error!( - "Expected digest {provided_digest} but computed {computed_digest}!" - ); - return Err(actix_web::error::ErrorBadRequest( - "Computed digest is different from the one provided in the JWT!", - )); - } - - Some(payload.to_vec()) - } - }; - - // Update last use (if needed) - if client.need_update_last_used() { - let mut user_up = user.clone(); - match user_up.find_client_by_id_mut(&client.id) { - None => log::error!("Client ID disappeared!!!"), - Some(u) => u.used = curr_time().unwrap(), - } - if let Err(e) = user_up.save().await { - log::error!("Failed to update last token usage! {e}"); - } - } - - Ok(Self { - client: Some(client.clone()), - payload, - user, - }) - } - - /// Get an instance of Matrix client - pub async fn client(&self) -> anyhow::Result { - self.user.matrix_client().await - } - - /// Send request to matrix server - pub async fn send_request, E: IncomingResponse>( - &self, - request: R, - ) -> anyhow::Result { - match self.client().await?.send_request(request).await { - Ok(e) => Ok(e), - Err(e) => Err(HttpFailure::MatrixClientError(e.to_string())), - } - } -} - -impl FromRequest for APIClientAuth { - type Error = actix_web::Error; - type Future = futures_util::future::LocalBoxFuture<'static, Result>; - - fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - let req = req.clone(); - - let remote_ip = match RemoteIP::from_request(&req, &mut Payload::None).into_inner() { - Ok(ip) => ip, - Err(e) => return Box::pin(async { Err(e) }), - }; - - let mut payload = payload.take(); - - Box::pin(async move { - let payload_bytes = match Bytes::from_request(&req, &mut payload).await { - Ok(b) => { - if b.is_empty() { - None - } else { - Some(b) - } - } - Err(e) => { - log::error!("Failed to extract request payload! {e}"); - None - } - }; - - Self::extract_auth(&req, remote_ip.0, payload_bytes).await - }) - } -} diff --git a/src/extractors/mod.rs b/src/extractors/mod.rs deleted file mode 100644 index f2e5226..0000000 --- a/src/extractors/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod client_auth; diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index e0979b6..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod app_config; -pub mod broadcast_messages; -pub mod constants; -pub mod extractors; -pub mod server; -pub mod sync_client; -pub mod user; -pub mod utils; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 7bc364c..0000000 --- a/src/main.rs +++ /dev/null @@ -1,80 +0,0 @@ -use actix_remote_ip::RemoteIPConfig; -use actix_session::config::SessionLifecycle; -use actix_session::{storage::RedisSessionStore, SessionMiddleware}; -use actix_web::cookie::Key; -use actix_web::{web, App, HttpServer}; -use matrix_gateway::app_config::AppConfig; -use matrix_gateway::broadcast_messages::BroadcastMessage; -use matrix_gateway::server::{api, web_ui}; -use matrix_gateway::sync_client; -use matrix_gateway::user::UserConfig; - -#[tokio::main] -async fn main() -> std::io::Result<()> { - env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); - - UserConfig::create_bucket_if_required() - .await - .expect("Failed to create bucket!"); - - let secret_key = Key::from(AppConfig::get().secret().as_bytes()); - - let redis_store = RedisSessionStore::new(AppConfig::get().redis_connection_string()) - .await - .expect("Failed to connect to Redis!"); - - let (ws_tx, _) = tokio::sync::broadcast::channel::(16); - - // Launch sync manager - tokio::spawn(sync_client::sync_client_manager(ws_tx.clone())); - - log::info!( - "Starting to listen on {} for {}", - AppConfig::get().listen_address, - AppConfig::get().website_origin - ); - HttpServer::new(move || { - App::new() - // Add session management to your application using Redis for session state storage - .wrap( - SessionMiddleware::builder(redis_store.clone(), secret_key.clone()) - .cookie_name("matrixgw-session".to_string()) - .session_lifecycle(SessionLifecycle::BrowserSession(Default::default())) - .build(), - ) - .app_data(web::Data::new(RemoteIPConfig { - proxy: AppConfig::get().proxy_ip.clone(), - })) - .app_data(web::Data::new(ws_tx.clone())) - // Web configuration routes - .route("/assets/{tail:.*}", web::get().to(web_ui::static_file)) - .route("/", web::get().to(web_ui::home)) - .route("/", web::post().to(web_ui::home)) - .route("/oidc_cb", web::get().to(web_ui::oidc_cb)) - .route("/sign_out", web::get().to(web_ui::sign_out)) - .route("/ws_debug", web::get().to(web_ui::ws_debug)) - // API routes - .route("/api", web::get().to(api::api_home)) - .route("/api", web::post().to(api::api_home)) - .route("/api/account/whoami", web::get().to(api::account::who_am_i)) - .route("/api/room/joined", web::get().to(api::room::joined_rooms)) - .route("/api/room/{room_id}", web::get().to(api::room::info)) - .route( - "/api/media/{server_name}/{media_id}/download", - web::get().to(api::media::download), - ) - .route( - "/api/media/{server_name}/{media_id}/thumbnail", - web::get().to(api::media::thumbnail), - ) - .route( - "/api/profile/{user_id}", - web::get().to(api::profile::get_profile), - ) - .service(web::resource("/api/ws").route(web::get().to(api::ws::ws))) - }) - .workers(4) - .bind(&AppConfig::get().listen_address)? - .run() - .await -} diff --git a/src/server/api/account.rs b/src/server/api/account.rs deleted file mode 100644 index 36d4731..0000000 --- a/src/server/api/account.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::extractors::client_auth::APIClientAuth; -use crate::server::HttpResult; -use actix_web::HttpResponse; -use ruma::api::client::account; -use ruma::DeviceId; - -#[derive(serde::Serialize)] -struct WhoAmIResponse { - user_id: String, - device_id: Option, -} - -/// Get current user identity -pub async fn who_am_i(auth: APIClientAuth) -> HttpResult { - let res = auth - .send_request(account::whoami::v3::Request::default()) - .await?; - - Ok(HttpResponse::Ok().json(WhoAmIResponse { - user_id: res.user_id.to_string(), - device_id: res.device_id.as_deref().map(DeviceId::to_string), - })) -} diff --git a/src/server/api/media.rs b/src/server/api/media.rs deleted file mode 100644 index 9b07e33..0000000 --- a/src/server/api/media.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::extractors::client_auth::APIClientAuth; -use crate::server::HttpResult; -use actix_web::{web, HttpResponse}; -use ruma::api::client::media; -use ruma::{OwnedServerName, UInt}; - -#[derive(serde::Deserialize)] -pub struct MediaInfoInPath { - server_name: OwnedServerName, - media_id: String, -} - -/// Download a media -#[allow(deprecated)] -pub async fn download(auth: APIClientAuth, path: web::Path) -> HttpResult { - let res = auth - .send_request(media::get_content::v3::Request::new( - path.media_id.clone(), - path.server_name.clone(), - )) - .await?; - - let mut http_res = HttpResponse::Ok(); - if let Some(content_type) = res.content_type { - http_res.content_type(content_type); - } - - Ok(http_res.body(res.file)) -} - -#[derive(serde::Deserialize)] -pub struct MediaThumbnailQuery { - width: Option, - height: Option, -} - -/// Get a media thumbnail -#[allow(deprecated)] -pub async fn thumbnail( - auth: APIClientAuth, - path: web::Path, - query: web::Query, -) -> HttpResult { - let res = auth - .send_request(media::get_content_thumbnail::v3::Request::new( - path.media_id.clone(), - path.server_name.clone(), - query.width.unwrap_or(UInt::new(500).unwrap()), - query.height.unwrap_or(UInt::new(500).unwrap()), - )) - .await?; - - let mut http_res = HttpResponse::Ok(); - if let Some(content_type) = res.content_type { - http_res.content_type(content_type); - } - - Ok(http_res.body(res.file)) -} diff --git a/src/server/api/mod.rs b/src/server/api/mod.rs deleted file mode 100644 index 73a26c8..0000000 --- a/src/server/api/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::extractors::client_auth::APIClientAuth; -use crate::server::HttpResult; -use actix_web::HttpResponse; - -pub mod account; -pub mod media; -pub mod profile; -pub mod room; -pub mod ws; - -/// API Home route -pub async fn api_home(auth: APIClientAuth) -> HttpResult { - Ok(HttpResponse::Ok().body(format!("Welcome user {}!", auth.user.user_id.0))) -} diff --git a/src/server/api/profile.rs b/src/server/api/profile.rs deleted file mode 100644 index d37e612..0000000 --- a/src/server/api/profile.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::extractors::client_auth::APIClientAuth; -use crate::server::HttpResult; -use crate::utils::matrix_utils::ApiMxcURI; -use actix_web::{web, HttpResponse}; -use ruma::api::client::profile; -use ruma::OwnedUserId; - -#[derive(serde::Deserialize)] -pub struct UserIDInPath { - user_id: OwnedUserId, -} - -#[derive(serde::Serialize)] -struct ProfileResponse { - display_name: Option, - avatar: Option, -} - -/// Get user profile -pub async fn get_profile(auth: APIClientAuth, path: web::Path) -> HttpResult { - let res = auth - .send_request(profile::get_profile::v3::Request::new(path.user_id.clone())) - .await?; - - Ok(HttpResponse::Ok().json(ProfileResponse { - display_name: res.displayname, - avatar: res.avatar_url.map(ApiMxcURI), - })) -} diff --git a/src/server/api/room.rs b/src/server/api/room.rs deleted file mode 100644 index 6afe1b5..0000000 --- a/src/server/api/room.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::extractors::client_auth::APIClientAuth; -use crate::server::{HttpFailure, HttpResult}; -use crate::utils::matrix_utils::ApiMxcURI; -use actix_web::{web, HttpResponse}; -use ruma::api::client::{membership, state}; -use ruma::events::StateEventType; -use ruma::{OwnedMxcUri, OwnedRoomId}; -use serde::de::DeserializeOwned; - -#[derive(serde::Serialize)] -struct GetRoomsMembershipsResponse { - rooms: Vec, -} - -/// Get the list of rooms the user has joined -pub async fn joined_rooms(auth: APIClientAuth) -> HttpResult { - let res = auth - .send_request(membership::joined_rooms::v3::Request::default()) - .await?; - - Ok(HttpResponse::Ok().json(GetRoomsMembershipsResponse { - rooms: res.joined_rooms, - })) -} - -#[derive(serde::Deserialize)] -pub struct RoomIDInPath { - room_id: OwnedRoomId, -} - -#[derive(serde::Serialize)] -struct GetRoomInfoResponse { - name: Option, - avatar: Option, -} - -/// Get a room information -async fn get_room_info( - auth: &APIClientAuth, - room_id: OwnedRoomId, - event_type: StateEventType, - field: &str, -) -> anyhow::Result, HttpFailure> { - let res = auth - .send_request(state::get_state_events_for_key::v3::Request::new( - room_id, - event_type, - String::default(), - )) - .await?; - - Ok(res.content.get_field(field)?) -} - -/// Get room information -pub async fn info(auth: APIClientAuth, path: web::Path) -> HttpResult { - let room_name: Option = get_room_info( - &auth, - path.room_id.clone(), - StateEventType::RoomName, - "name", - ) - .await - .ok() - .flatten(); - - let room_avatar: Option = get_room_info( - &auth, - path.room_id.clone(), - StateEventType::RoomAvatar, - "url", - ) - .await - .ok() - .flatten(); - - Ok(HttpResponse::Ok().json(GetRoomInfoResponse { - name: room_name, - avatar: room_avatar.map(ApiMxcURI), - })) -} diff --git a/src/server/api/ws.rs b/src/server/api/ws.rs deleted file mode 100644 index 3909956..0000000 --- a/src/server/api/ws.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::broadcast_messages::{BroadcastMessage, SyncEvent}; -use crate::constants::{WS_CLIENT_TIMEOUT, WS_HEARTBEAT_INTERVAL}; -use crate::extractors::client_auth::APIClientAuth; -use crate::server::HttpResult; -use actix_web::dev::Payload; -use actix_web::{web, FromRequest, HttpRequest}; -use actix_ws::Message; -use futures_util::StreamExt; -use std::time::Instant; -use tokio::select; -use tokio::sync::broadcast; -use tokio::sync::broadcast::Receiver; -use tokio::time::interval; - -/// Messages send to the client -#[derive(Debug, serde::Deserialize, serde::Serialize)] -#[serde(tag = "type")] -pub enum WsMessage { - Sync(SyncEvent), -} - -/// Main WS route -pub async fn ws( - req: HttpRequest, - stream: web::Payload, - tx: web::Data>, -) -> HttpResult { - // Forcefully ignore request payload by manually extracting authentication information - let auth = APIClientAuth::from_request(&req, &mut Payload::None).await?; - - let (res, session, msg_stream) = actix_ws::handle(&req, stream)?; - - // Ask for sync client to be started - if let Err(e) = tx.send(BroadcastMessage::StartSyncTaskForUser( - auth.user.user_id.clone(), - )) { - log::error!("Failed to send StartSyncTaskForUser: {e}"); - } - - let rx = tx.subscribe(); - - // spawn websocket handler (and don't await it) so that the response is returned immediately - actix_web::rt::spawn(ws_handler(session, msg_stream, auth, rx)); - - Ok(res) -} - -pub async fn ws_handler( - mut session: actix_ws::Session, - mut msg_stream: actix_ws::MessageStream, - auth: APIClientAuth, - mut rx: Receiver, -) { - log::info!("WS connected"); - - let mut last_heartbeat = Instant::now(); - let mut interval = interval(WS_HEARTBEAT_INTERVAL); - - let reason = loop { - // waits for either `msg_stream` to receive a message from the client, the broadcast channel - // to send a message, or the heartbeat interval timer to tick, yielding the value of - // whichever one is ready first - select! { - ws_msg = rx.recv() => { - let msg = match ws_msg { - Ok(msg) => msg, - Err(broadcast::error::RecvError::Closed) => break None, - Err(broadcast::error::RecvError::Lagged(_)) => continue, - }; - - match msg { - BroadcastMessage::CloseClientSession(id) => { - if let Some(client) = &auth.client { - if client.id == id { - log::info!( - "closing client session {id:?} of user {:?} as requested", auth.user.user_id - ); - break None; - } - } - }, - BroadcastMessage::CloseAllUserSessions(userid) => { - if userid == auth.user.user_id { - log::info!( - "closing WS session of user {userid:?} as requested" - ); - break None; - } - } - - BroadcastMessage::SyncEvent(userid, event) => { - if userid != auth.user.user_id { - continue; - } - - // Send the message to the websocket - if let Ok(msg) = serde_json::to_string(&WsMessage::Sync(*event)) { - if let Err(e) = session.text(msg).await { - log::error!("Failed to send SyncEvent: {e}"); - } - } - } - _ => {}}; - - } - - // heartbeat interval ticked - _tick = interval.tick() => { - // if no heartbeat ping/pong received recently, close the connection - if Instant::now().duration_since(last_heartbeat) > WS_CLIENT_TIMEOUT { - log::info!( - "client has not sent heartbeat in over {WS_CLIENT_TIMEOUT:?}; disconnecting" - ); - - break None; - } - - // send heartbeat ping - let _ = session.ping(b"").await; - }, - - msg = msg_stream.next() => { - let msg = match msg { - // received message from WebSocket client - Some(Ok(msg)) => msg, - - // client WebSocket stream error - Some(Err(err)) => { - log::error!("{err}"); - break None; - } - - // client WebSocket stream ended - None => break None - }; - - log::debug!("msg: {msg:?}"); - - match msg { - Message::Text(s) => { - log::info!("Text message: {s}"); - } - - Message::Binary(_) => { - // drop client's binary messages - } - - Message::Close(reason) => { - break reason; - } - - Message::Ping(bytes) => { - last_heartbeat = Instant::now(); - let _ = session.pong(&bytes).await; - } - - Message::Pong(_) => { - last_heartbeat = Instant::now(); - } - - Message::Continuation(_) => { - log::warn!("no support for continuation frames"); - } - - // no-op; ignore - Message::Nop => {} - }; - } - } - }; - - // attempt to close connection gracefully - let _ = session.close(reason).await; - - log::info!("WS disconnected"); -} diff --git a/src/server/mod.rs b/src/server/mod.rs deleted file mode 100644 index 007cd9f..0000000 --- a/src/server/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -use actix_web::http::StatusCode; -use actix_web::{HttpResponse, ResponseError}; -use std::error::Error; -use std::fmt::Debug; - -pub mod api; -pub mod web_ui; - -#[derive(thiserror::Error, Debug)] -pub enum HttpFailure { - #[error("this resource requires higher privileges")] - Forbidden, - #[error("this resource was not found")] - NotFound, - #[error("Actix web error")] - ActixError(#[from] actix_web::Error), - #[error("an unhandled session insert error occurred")] - SessionInsertError(#[from] actix_session::SessionInsertError), - #[error("an unhandled session error occurred")] - SessionError(#[from] actix_session::SessionGetError), - #[error("an unspecified open id error occurred: {0}")] - OpenID(Box), - #[error("an error occurred while fetching user configuration: {0}")] - FetchUserConfig(anyhow::Error), - #[error("an unspecified internal error occurred: {0}")] - InternalError(#[from] anyhow::Error), - #[error("a matrix api client error occurred: {0}")] - MatrixApiClientError(#[from] ruma::api::client::Error), - #[error("a matrix client error occurred: {0}")] - MatrixClientError(String), - #[error("a serde_json error occurred: {0}")] - SerdeJsonError(#[from] serde_json::error::Error), -} - -impl ResponseError for HttpFailure { - fn status_code(&self) -> StatusCode { - match &self { - Self::Forbidden => StatusCode::FORBIDDEN, - Self::NotFound => StatusCode::NOT_FOUND, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()).body(self.to_string()) - } -} - -pub type HttpResult = Result; diff --git a/src/server/web_ui.rs b/src/server/web_ui.rs deleted file mode 100644 index e8f68b1..0000000 --- a/src/server/web_ui.rs +++ /dev/null @@ -1,252 +0,0 @@ -use crate::app_config::AppConfig; -use crate::broadcast_messages::BroadcastMessage; -use crate::constants::{STATE_KEY, USER_SESSION_KEY}; -use crate::server::{HttpFailure, HttpResult}; -use crate::user::{APIClient, APIClientID, User, UserConfig, UserID}; -use crate::utils::base_utils; -use actix_session::Session; -use actix_web::{web, HttpResponse}; -use askama::Template; -use ipnet::IpNet; -use light_openid::primitives::OpenIDConfig; -use std::str::FromStr; -use tokio::sync::broadcast; - -/// Static assets -#[derive(rust_embed::Embed)] -#[folder = "assets/"] -struct Assets; - -/// Serve static file -pub async fn static_file(path: web::Path) -> HttpResult { - match Assets::get(path.as_ref()) { - Some(content) => Ok(HttpResponse::Ok() - .content_type( - mime_guess::from_path(path.as_str()) - .first_or_octet_stream() - .as_ref(), - ) - .body(content.data.into_owned())), - None => Ok(HttpResponse::NotFound().body("404 Not Found")), - } -} - -#[derive(askama::Template)] -#[template(path = "index.html")] -struct HomeTemplate { - name: String, - user_id: UserID, - matrix_token: String, - clients: Vec, - success_message: Option, - error_message: Option, -} - -/// HTTP form request -#[derive(serde::Deserialize)] -pub struct FormRequest { - /// Update matrix token - new_matrix_token: Option, - - /// Create a new client - new_client_desc: Option, - - /// Restrict new client to a given network - ip_network: Option, - - /// Grant read only access to client - readonly_client: Option, - - /// Delete a specified client id - delete_client_id: Option, -} - -/// Main route -pub async fn home( - session: Session, - form_req: Option>, - tx: web::Data>, -) -> HttpResult { - // Get user information, requesting authentication if information is missing - let Some(user): Option = session.get(USER_SESSION_KEY)? else { - // Generate auth state - let state = base_utils::rand_str(50); - session.insert(STATE_KEY, &state)?; - - let oidc = AppConfig::get().openid_provider(); - let config = OpenIDConfig::load_from_url(oidc.configuration_url) - .await - .map_err(HttpFailure::OpenID)?; - - let auth_url = config.gen_authorization_url(oidc.client_id, &state, &oidc.redirect_url); - - return Ok(HttpResponse::Found() - .append_header(("location", auth_url)) - .finish()); - }; - - let mut success_message = None; - let mut error_message = None; - - // Retrieve user configuration - let mut config = UserConfig::load(&user.id, true) - .await - .map_err(HttpFailure::FetchUserConfig)?; - - if let Some(form_req) = form_req { - // Update matrix token, if requested - if let Some(t) = form_req.0.new_matrix_token { - if t.len() < 3 { - error_message = Some("Specified Matrix token is too short!".to_string()); - } else { - config.matrix_token = t; - config.save().await?; - success_message = Some("Matrix token was successfully updated!".to_string()); - - // Close sync task - if let Err(e) = tx.send(BroadcastMessage::StopSyncTaskForUser(user.id.clone())) { - log::error!("Failed to send StopSyncClientForUser: {e}"); - } - - // Invalidate all Ws connections - if let Err(e) = tx.send(BroadcastMessage::CloseAllUserSessions(user.id.clone())) { - log::error!("Failed to send CloseAllUserSessions: {e}"); - } - } - } - - // Create a new client, if requested - if let Some(new_token_desc) = form_req.0.new_client_desc { - let ip_net = match form_req.0.ip_network.as_deref() { - None | Some("") => None, - Some(e) => match IpNet::from_str(e) { - Ok(n) => Some(n), - Err(e) => { - log::error!("Failed to parse IP network provided by user: {e}"); - error_message = Some(format!("Failed to parse restricted IP network: {e}")); - None - } - }, - }; - - if error_message.is_none() { - let mut token = APIClient::generate(new_token_desc, ip_net); - token.readonly_client = form_req.0.readonly_client.is_some(); - success_message = Some(format!("The secret of your new token is '{}'. Be sure to write it somewhere as you will not be able to recover it later!", token.secret)); - config.clients.push(token); - config.save().await?; - } - } - - // Delete a client - if let Some(delete_client_id) = form_req.0.delete_client_id { - config.clients.retain(|c| c.id != delete_client_id); - config.save().await?; - success_message = Some("The client was successfully deleted!".to_string()); - - if let Err(e) = tx.send(BroadcastMessage::CloseClientSession(delete_client_id)) { - log::error!("Failed to send CloseClientSession: {e}"); - } - } - } - - // Render page - Ok(HttpResponse::Ok() - .insert_header(("content-type", "text/html")) - .body( - HomeTemplate { - name: user.name, - user_id: user.id, - matrix_token: config.obfuscated_matrix_token(), - clients: config.clients, - success_message, - error_message, - } - .render() - .unwrap(), - )) -} - -#[derive(serde::Deserialize)] -pub struct AuthCallbackQuery { - code: String, - state: String, -} - -/// Authenticate user callback -pub async fn oidc_cb(session: Session, query: web::Query) -> HttpResult { - if session.get(STATE_KEY)? != Some(query.state.to_string()) { - return Ok(HttpResponse::BadRequest() - .append_header(("content-type", "text/html")) - .body("State mismatch! Try again")); - } - - let oidc = AppConfig::get().openid_provider(); - let config = OpenIDConfig::load_from_url(oidc.configuration_url) - .await - .map_err(HttpFailure::OpenID)?; - - let (token, _) = match config - .request_token( - oidc.client_id, - oidc.client_secret, - &query.code, - &oidc.redirect_url, - ) - .await - { - Ok(t) => t, - Err(e) => { - log::error!("Failed to request user token! {e}"); - - return Ok(HttpResponse::BadRequest() - .append_header(("content-type", "text/html")) - .body("Authentication failed! Try again")); - } - }; - - let (user, _) = config - .request_user_info(&token) - .await - .map_err(HttpFailure::OpenID)?; - - let user = User { - id: UserID(user.sub), - name: user.name.unwrap_or("no_name".to_string()), - email: user.email.unwrap_or("no@mail.com".to_string()), - }; - log::info!("Successful authentication as {user:?}"); - session.insert(USER_SESSION_KEY, user)?; - - Ok(HttpResponse::Found() - .insert_header(("location", "/")) - .finish()) -} - -/// De-authenticate user -pub async fn sign_out(session: Session) -> HttpResult { - session.remove(USER_SESSION_KEY); - - Ok(HttpResponse::Found() - .insert_header(("location", "/")) - .finish()) -} - -#[derive(askama::Template)] -#[template(path = "ws_debug.html")] -struct WsDebugTemplate { - name: String, -} - -/// WebSocket debug -pub async fn ws_debug(session: Session) -> HttpResult { - let Some(user): Option = session.get(USER_SESSION_KEY)? else { - return Ok(HttpResponse::Found() - .insert_header(("location", "/")) - .finish()); - }; - - Ok(HttpResponse::Ok() - .content_type("text/html") - .body(WsDebugTemplate { name: user.name }.render().unwrap())) -} diff --git a/src/sync_client.rs b/src/sync_client.rs deleted file mode 100644 index a2b9fb2..0000000 --- a/src/sync_client.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::broadcast_messages::{BroadcastMessage, SyncEvent}; -use crate::user::{UserConfig, UserID}; -use futures_util::TryStreamExt; -use ruma::api::client::sync::sync_events; -use ruma::assign; -use ruma::presence::PresenceState; -use std::collections::HashMap; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::broadcast; - -/// ID of sync client -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct SyncClientID(uuid::Uuid); - -/// Sync client launcher loop -pub async fn sync_client_manager(tx: broadcast::Sender) -> ! { - let mut rx = tx.subscribe(); - let tx = Arc::new(tx.clone()); - - let mut running_tasks = HashMap::new(); - - while let Ok(msg) = rx.recv().await { - match msg { - BroadcastMessage::StartSyncTaskForUser(user_id) => { - if running_tasks.contains_key(&user_id) { - log::info!("Won't start sync task for user {user_id:?} because a task is already running for this user!"); - continue; - } - - log::info!("Start sync task for user {user_id:?}"); - let task_id = SyncClientID(uuid::Uuid::new_v4()); - running_tasks.insert(user_id.clone(), task_id.clone()); - - let tx = tx.clone(); - tokio::task::spawn(async move { - sync_task(task_id, user_id, tx).await; - }); - } - - BroadcastMessage::StopSyncTaskForUser(user_id) => { - // Check if a task is running for this user - if let Some(task_id) = running_tasks.remove(&user_id) { - log::info!("Stop sync task for user {user_id:?}"); - tx.send(BroadcastMessage::StopSyncClient(task_id)).unwrap(); - } else { - log::info!("Not stopping sync task for user {user_id:?}: not running"); - } - } - - _ => {} - } - } - - panic!("Sync client manager stopped unexpectedly!"); -} - -/// Sync task for a single user -async fn sync_task( - id: SyncClientID, - user_id: UserID, - tx: Arc>, -) { - let mut rx = tx.subscribe(); - - let Ok(user_config) = UserConfig::load(&user_id, false).await else { - log::error!("Failed to load user config in sync thread!"); - return; - }; - - let client = match user_config.matrix_client().await { - Err(e) => { - log::error!("Failed to load matrix client for user {user_id:?}: {e}"); - return; - } - Ok(client) => client, - }; - - let initial_sync_response = match client - .send_request(assign!(sync_events::v3::Request::new(), { - filter: None, - })) - .await - { - Ok(res) => res, - Err(e) => { - log::error!("Failed to perform initial sync request for user {user_id:?}! {e}"); - return; - } - }; - - let mut sync_stream = Box::pin(client.sync( - None, - initial_sync_response.next_batch, - PresenceState::Offline, - Some(Duration::from_secs(30)), - )); - - loop { - tokio::select! { - // Message from tokio broadcast - msg = rx.recv() => { - match msg { - Ok(BroadcastMessage::StopSyncClient(client_id)) => { - if client_id == id { - log::info!("A request was received to stop this client! {id:?} for user {user_id:?}"); - break; - } - } - - Err(e) => { - log::error!("Failed to receive a message from broadcast! {e}"); - return; - } - - Ok(_) => {} - } - } - - // Message from Matrix - msg_stream = sync_stream.try_next() => { - match msg_stream { - Ok(Some(msg)) => { - log::debug!("Received new message from Matrix: {msg:#?}"); - if let Err(e) = tx.send(BroadcastMessage::SyncEvent(user_id.clone(), Box::new(SyncEvent { - rooms: msg.rooms,presence: msg.presence, - account_data: msg.account_data, - to_device: msg.to_device, - device_lists: msg.device_lists, - }))) { - log::error!("Failed to propagate event! {e}"); - } - } - Ok(None) => { - log::debug!("Received no message from Matrix"); - } - Err(e) => { - log::error!("Failed to receive a message from Matrix! {e}"); - return; - } - } - } - } - } -} diff --git a/src/user.rs b/src/user.rs deleted file mode 100644 index c7397f0..0000000 --- a/src/user.rs +++ /dev/null @@ -1,241 +0,0 @@ -use s3::error::S3Error; -use s3::request::ResponseData; -use s3::{Bucket, BucketConfiguration}; -use std::str::FromStr; -use thiserror::Error; - -use crate::app_config::AppConfig; -use crate::constants::TOKEN_LEN; -use crate::utils::base_utils::{curr_time, format_time, rand_str}; - -type HttpClient = ruma::client::http_client::HyperNativeTls; -pub type RumaClient = ruma::Client; - -#[derive(Error, Debug)] -pub enum UserError { - #[error("failed to fetch user configuration: {0}")] - FetchUserConfig(S3Error), - #[error("missing matrix token")] - MissingMatrixToken, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct UserID(pub String); - -impl UserID { - fn conf_path_in_bucket(&self) -> String { - format!("confs/{}.json", urlencoding::encode(&self.0)) - } -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct User { - pub id: UserID, - pub name: String, - pub email: String, -} - -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] -pub struct APIClientID(pub uuid::Uuid); - -impl APIClientID { - pub fn generate() -> Self { - Self(uuid::Uuid::new_v4()) - } -} - -impl FromStr for APIClientID { - type Err = uuid::Error; - - fn from_str(s: &str) -> Result { - Ok(Self(uuid::Uuid::from_str(s)?)) - } -} - -/// Single API client information -#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct APIClient { - /// Client unique ID - pub id: APIClientID, - - /// Client description - pub description: String, - - /// Restricted API network for token - pub network: Option, - - /// Client secret - pub secret: String, - - /// Client creation time - pub created: u64, - - /// Client last usage time - pub used: u64, - - /// Read only access - pub readonly_client: bool, -} - -impl APIClient { - pub fn fmt_created(&self) -> String { - format_time(self.created).unwrap_or_default() - } - - pub fn fmt_used(&self) -> String { - format_time(self.used).unwrap_or_default() - } - - pub fn need_update_last_used(&self) -> bool { - self.used + 60 * 15 < curr_time().unwrap() - } -} - -impl APIClient { - /// Generate a new API client - pub fn generate(description: String, network: Option) -> Self { - Self { - id: APIClientID::generate(), - description, - network, - secret: rand_str(TOKEN_LEN), - created: curr_time().unwrap(), - used: curr_time().unwrap(), - readonly_client: true, - } - } -} - -#[derive(serde::Serialize, serde::Deserialize, Clone)] -pub struct UserConfig { - /// Target user ID - pub user_id: UserID, - - /// Configuration creation time - pub created: u64, - - /// Configuration last update time - pub updated: u64, - - /// Current user matrix token - pub matrix_token: String, - - /// API clients - pub clients: Vec, -} - -impl UserConfig { - /// Create S3 bucket if required - pub async fn create_bucket_if_required() -> anyhow::Result<()> { - if AppConfig::get().s3_skip_auto_create_bucket { - log::debug!("Skipping bucket existence check"); - return Ok(()); - } - - let bucket = AppConfig::get().s3_bucket()?; - - match bucket.location().await { - Ok(_) => { - log::debug!("The bucket already exists."); - return Ok(()); - } - Err(S3Error::HttpFailWithBody(404, s)) if s.contains("NoSuchKey") => { - log::warn!("Failed to fetch bucket location, but it seems that bucket exists."); - return Ok(()); - } - Err(S3Error::HttpFailWithBody(404, s)) if s.contains("NoSuchBucket") => { - log::warn!("The bucket does not seem to exists, trying to create it!") - } - Err(e) => { - log::error!("Got unexpected error when querying bucket info: {e}"); - return Err(e.into()); - } - } - - Bucket::create_with_path_style( - &bucket.name, - bucket.region, - AppConfig::get().s3_credentials()?, - BucketConfiguration::private(), - ) - .await?; - - Ok(()) - } - - /// Get current user configuration - pub async fn load(user_id: &UserID, allow_non_existing: bool) -> anyhow::Result { - let res: Result = AppConfig::get() - .s3_bucket()? - .get_object(user_id.conf_path_in_bucket()) - .await; - - match (res, allow_non_existing) { - (Ok(res), _) => Ok(serde_json::from_slice(res.as_slice())?), - (Err(S3Error::HttpFailWithBody(404, _)), true) => { - log::warn!("User configuration does not exists, generating a new one..."); - Ok(Self { - user_id: user_id.clone(), - created: curr_time()?, - updated: curr_time()?, - matrix_token: "".to_string(), - clients: vec![], - }) - } - (Err(e), _) => Err(UserError::FetchUserConfig(e).into()), - } - } - - /// Set user configuration - pub async fn save(&mut self) -> anyhow::Result<()> { - log::info!("Saving new configuration for user {:?}", self.user_id); - - self.updated = curr_time()?; - - // Save updated configuration - AppConfig::get() - .s3_bucket()? - .put_object( - self.user_id.conf_path_in_bucket(), - &serde_json::to_vec(self)?, - ) - .await?; - - Ok(()) - } - - /// Get current user matrix token, in an obfuscated form - pub fn obfuscated_matrix_token(&self) -> String { - self.matrix_token - .chars() - .enumerate() - .map(|(num, c)| match num { - 0 | 1 => c, - _ => 'X', - }) - .collect() - } - - /// Find a client by its id - pub fn find_client_by_id(&self, id: &APIClientID) -> Option<&APIClient> { - self.clients.iter().find(|c| &c.id == id) - } - - /// Find a client by its id and get a mutable reference - pub fn find_client_by_id_mut(&mut self, id: &APIClientID) -> Option<&mut APIClient> { - self.clients.iter_mut().find(|c| &c.id == id) - } - - /// Get a matrix client instance for the current user - pub async fn matrix_client(&self) -> anyhow::Result { - if self.matrix_token.is_empty() { - return Err(UserError::MissingMatrixToken.into()); - } - - Ok(ruma::Client::builder() - .homeserver_url(AppConfig::get().matrix_homeserver.to_string()) - .access_token(Some(self.matrix_token.clone())) - .build() - .await?) - } -} diff --git a/src/utils/base_utils.rs b/src/utils/base_utils.rs deleted file mode 100644 index 68a919f..0000000 --- a/src/utils/base_utils.rs +++ /dev/null @@ -1,20 +0,0 @@ -use rand::distr::{Alphanumeric, SampleString}; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// Generate a random string of a given size -pub fn rand_str(len: usize) -> String { - Alphanumeric.sample_string(&mut rand::rng(), len) -} - -/// Get current time -pub fn curr_time() -> anyhow::Result { - Ok(SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|t| t.as_secs())?) -} - -/// Format time -pub fn format_time(time: u64) -> Option { - let time = chrono::DateTime::from_timestamp(time as i64, 0)?; - Some(time.naive_local().to_string()) -} diff --git a/src/utils/matrix_utils.rs b/src/utils/matrix_utils.rs deleted file mode 100644 index f780db9..0000000 --- a/src/utils/matrix_utils.rs +++ /dev/null @@ -1,18 +0,0 @@ -use ruma::OwnedMxcUri; -use serde::ser::SerializeMap; -use serde::{Serialize, Serializer}; - -pub struct ApiMxcURI(pub OwnedMxcUri); - -impl Serialize for ApiMxcURI { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(Some(3))?; - map.serialize_entry("uri", &self.0)?; - map.serialize_entry("server_name", &self.0.server_name().ok())?; - map.serialize_entry("media_id", &self.0.media_id().ok())?; - map.end() - } -} diff --git a/src/utils/mod.rs b/src/utils/mod.rs deleted file mode 100644 index a45b810..0000000 --- a/src/utils/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod base_utils; -pub mod matrix_utils; diff --git a/templates/base_page.html b/templates/base_page.html deleted file mode 100644 index f86c767..0000000 --- a/templates/base_page.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - Matrix GW - - - - - - - - -
- -
- - -
- {% block content %} - TO_REPLACE - {% endblock content %} -
- - - - \ No newline at end of file diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index f1360f1..0000000 --- a/templates/index.html +++ /dev/null @@ -1,146 +0,0 @@ -{% extends "base_page.html" %} -{% block content %} - -{% if let Some(msg) = success_message %} -
- {{ msg }} -
-{% endif %} - - -{% if let Some(msg) = error_message %} -
- {{ msg }} -
-{% endif %} - - -
Current user ID: {{ user_id.0 }}
- - -
-
Registered clients
-
- {% if clients.len() > 0 %} - - - - - - - - - - - - - - {% for client in clients %} - - - - - - - - - - {% endfor %} - -
IDDescriptionRead onlyNetworkCreatedUsed
{{ client.id.0 }}{{ client.description }} - {% if client.readonly_client %} - YES - {% else %} - NO - {% endif %} - - {% if let Some(net) = client.network %} - {{ net }} - {% else %} - Unrestricted - {% endif %} - {{ client.fmt_created() }}{{ client.fmt_used() }} - -
- {% endif %} - - {% if clients.len() == 0 %} -

No client registered yet!

- {% endif %} -
-
- - -
-
New client
-
-
-
- - - Client description helps with identification. -
-
- - - Restrict the networks this IP address can be used from. -
- -
- -
- - -
- -
- - - -
-
-
- - -
-
Matrix authentication token
-
-

To obtain a new Matrix authentication token:

-
    -
  1. Sign in to Element from a private browser window
  2. -
  3. Open All settings and access the Help & About tag
  4. -
  5. Expand Access Token and copy the value
  6. -
  7. Paste the copied value below
  8. -
  9. Close the private browser window without signing out!
  10. -
- -

You should not need to replace this value unless you explicitly signed out the associated browser - session.

- -

Tip: you can rename the session to easily identify it among all your other sessions!

- -
-
- - - Changing this value will reset all active - connections - to Matrix GW. -
- - -
-
-
- - -{% endblock content %} \ No newline at end of file diff --git a/templates/ws_debug.html b/templates/ws_debug.html deleted file mode 100644 index d3f9119..0000000 --- a/templates/ws_debug.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "base_page.html" %} -{% block content %} - - - - -

WS Debug

- -
- - - - State: DISCONNECTED -
- -
-
-
INFO
-
Welcome!
-
-
- - -{% endblock content %} \ No newline at end of file From 602edaae79ef5437fd9313ce3957fb61fe981737 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 31 Oct 2025 18:29:31 +0100 Subject: [PATCH 002/124] Start to build backend base --- README.md | 18 +- matrixgw_backend/Cargo.lock | 3097 +++++++++++++++++++ matrixgw_backend/Cargo.toml | 10 + matrixgw_backend/docker-compose.yml | 12 +- matrixgw_backend/docker/dex/dex.config.yaml | 26 + matrixgw_backend/src/app_config.rs | 187 ++ matrixgw_backend/src/lib.rs | 1 + matrixgw_backend/src/main.rs | 28 +- 8 files changed, 3373 insertions(+), 6 deletions(-) create mode 100644 matrixgw_backend/Cargo.lock create mode 100644 matrixgw_backend/docker/dex/dex.config.yaml create mode 100644 matrixgw_backend/src/app_config.rs create mode 100644 matrixgw_backend/src/lib.rs diff --git a/README.md b/README.md index 62e7b9b..8cc8aa6 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,9 @@ Project that expose a simple API to make use of Matrix API. It acts as a Matrix **Known limitations**: - Supports only a limited subset of Matrix API -- Does not support E2E encryption - Does not support spaces -Project written in Rust. Releases are published on Docker Hub. +Project written in Rust and TypeScript. Releases are published on Docker Hub. ## Docker image options ```bash @@ -17,7 +16,10 @@ docker run --rm -it docker.io/pierre42100/matrix_gateway --help ``` ## Setup dev environment + +### Dependencies ``` +cd matrixgw_backend mkdir -p storage/maspostgres storage/synapse storage/minio docker compose up ``` @@ -42,3 +44,15 @@ Auto-created Matrix accounts: Minio administration credentials: `minioadmin` : `minioadmin` +### Backend +```bash +cd matrixgw_backend +cargo fmt && cargo clippy && cargo run -- +``` + +### Frontend +```bash +cd matrixgw_frontend +npm install +npm run dev +``` \ No newline at end of file diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock new file mode 100644 index 0000000..079c0e7 --- /dev/null +++ b/matrixgw_backend/Cargo.lock @@ -0,0 +1,3097 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7926860314cbe2fb5d1f13731e387ab43bd32bca224e82e6e2db85de0a3dba49" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "base64 0.22.1", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "foldhash", + "futures-core", + "h2", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.9.2", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2 0.5.10", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "actix-session" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "400c27fd4cdbe0082b7bbd29ac44a3070cbda1b2114138dc106ba39fe2f90dff" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "anyhow", + "derive_more", + "rand 0.9.2", + "redis", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "foldhash", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2 0.5.10", + "time", + "tracing", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http 1.3.1", + "log", + "native-tls", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-creds" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13804829a843b3f26e151c97acbb315ee1177a2724690edfcd28f1894146200" +dependencies = [ + "attohttpc", + "home", + "log", + "quick-xml", + "rust-ini", + "serde", + "thiserror", + "time", + "url", +] + +[[package]] +name = "aws-region" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5532f65342f789f9c1b7078ea9c9cd9293cd62dcc284fa99adc4a1c9ba43469c" +dependencies = [ + "thiserror", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", +] + +[[package]] +name = "base64" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytestring" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" +dependencies = [ + "bytes", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[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 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "aes-gcm", + "base64 0.20.0", + "hkdf", + "hmac", + "percent-encoding", + "rand 0.8.5", + "sha2", + "subtle", + "time", + "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-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.3.1", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-more" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "matrixgw_backend" +version = "0.1.0" +dependencies = [ + "actix-session", + "actix-web", + "anyhow", + "clap", + "env_logger", + "lazy_static", + "log", + "rust-s3", + "serde", + "tokio", +] + +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minidom" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e394a0e3c7ccc2daea3dffabe82f09857b6b510cb25af87d54bf3e910ac1642d" +dependencies = [ + "rxml", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +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-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redis" +version = "0.32.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "014cc767fefab6a3e798ca45112bccad9c6e0e218fbd49720042716c73cfef44" +dependencies = [ + "arc-swap", + "backon", + "bytes", + "cfg-if", + "combine", + "futures-channel", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "socket2 0.6.1", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust-s3" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f9b973bd4097f5bb47e5827dcb9fb5dc17e93879e46badc27d2a4e9a4e5588" +dependencies = [ + "async-trait", + "aws-creds", + "aws-region", + "base64 0.22.1", + "bytes", + "cfg-if", + "futures-util", + "hex", + "hmac", + "http 1.3.1", + "log", + "maybe-async", + "md5", + "minidom", + "percent-encoding", + "quick-xml", + "reqwest", + "serde", + "serde_derive", + "serde_json", + "sha2", + "sysinfo", + "thiserror", + "time", + "tokio", + "tokio-stream", + "url", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rxml" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc94b580d0f5a6b7a2d604e597513d3c673154b52ddeccd1d5c32360d945ee" +dependencies = [ + "bytes", + "rxml_validation", +] + +[[package]] +name = "rxml_validation" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826e80413b9a35e9d33217b3dcac04cf95f6559d15944b93887a08be5496c4a4" +dependencies = [ + "compact_str", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sysinfo" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "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 = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.3.1", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +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 = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index ab0c915..c282d75 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -4,3 +4,13 @@ version = "0.1.0" edition = "2024" [dependencies] +env_logger = "0.11.8" +log = "0.4.28" +clap = { version = "4.5.51", features = ["derive", "env"] } +lazy_static = "1.5.0" +anyhow = "1.0.100" +serde = { version = "1.0.228", features = ["derive"] } +rust-s3 = { version = "0.37.0", features = ["tokio"] } +tokio = { version = "1.48.0", features = ["full"] } +actix-web = "4.11.0" +actix-session = { version = "0.11.0", features = ["redis-session"] } \ No newline at end of file diff --git a/matrixgw_backend/docker-compose.yml b/matrixgw_backend/docker-compose.yml index 26d2b1d..fec7c62 100644 --- a/matrixgw_backend/docker-compose.yml +++ b/matrixgw_backend/docker-compose.yml @@ -80,10 +80,18 @@ services: element: image: docker.io/vectorim/element-web ports: - - 8080:80/tcp + - "8080:80/tcp" volumes: - ./docker/element/config.json:/app/config.json:ro + oidc: + image: dexidp/dex + ports: + - "9001:9001" + volumes: + - ./docker/dex:/conf:ro + command: [ "dex", "serve", "/conf/dex.config.yaml" ] + minio: image: quay.io/minio/minio command: minio server --console-address ":9002" /data @@ -101,7 +109,7 @@ services: image: redis:alpine command: redis-server --requirepass ${REDIS_PASS:-secretredis} ports: - - 6379:6379 + - "6379:6379" volumes: - ./storage/redis-data:/data - ./storage/redis-conf:/usr/local/etc/redis/redis.conf diff --git a/matrixgw_backend/docker/dex/dex.config.yaml b/matrixgw_backend/docker/dex/dex.config.yaml new file mode 100644 index 0000000..7704800 --- /dev/null +++ b/matrixgw_backend/docker/dex/dex.config.yaml @@ -0,0 +1,26 @@ +issuer: http://127.0.0.1:9001/dex + +storage: + type: memory + +web: + http: 0.0.0.0:9001 + +oauth2: + # Automate some clicking + # Note: this might actually make some tests pass that otherwise wouldn't. + skipApprovalScreen: false + +connectors: + # Note: this might actually make some tests pass that otherwise wouldn't. + - type: mockCallback + id: mock + name: Example + +# Basic OP test suite requires two clients. +staticClients: + - id: foo + secret: bar + redirectURIs: + - http://localhost:8000/oidc_cb + name: Project diff --git a/matrixgw_backend/src/app_config.rs b/matrixgw_backend/src/app_config.rs new file mode 100644 index 0000000..a1271d7 --- /dev/null +++ b/matrixgw_backend/src/app_config.rs @@ -0,0 +1,187 @@ +use clap::Parser; +use s3::creds::Credentials; +use s3::{Bucket, Region}; + +/// Matrix gateway backend API +#[derive(Parser, Debug, Clone)] +#[clap(author, version, about, long_about = None)] +pub struct AppConfig { + /// Listen address + #[clap(short, long, env, default_value = "0.0.0.0:8000")] + pub listen_address: String, + + /// Website origin + #[clap(short, long, env, default_value = "http://localhost:8000")] + pub website_origin: String, + + /// Proxy IP, might end with a star "*" + #[clap(short, long, env)] + pub proxy_ip: Option, + + /// Secret key, used to secure some resources. Must be randomly generated + #[clap(short = 'S', long, env, default_value = "")] + secret: String, + + /// Matrix homeserver origin + #[clap(short, long, env, default_value = "http://127.0.0.1:8448")] + pub matrix_homeserver: String, + + /// Redis connection hostname + #[clap(long, env, default_value = "localhost")] + redis_hostname: String, + + /// Redis connection port + #[clap(long, env, default_value_t = 6379)] + redis_port: u16, + + /// Redis database number + #[clap(long, env, default_value_t = 0)] + redis_db_number: i64, + + /// Redis username + #[clap(long, env)] + redis_username: Option, + + /// Redis password + #[clap(long, env, default_value = "secretredis")] + redis_password: String, + + /// URL where the OpenID configuration can be found + #[arg( + long, + env, + default_value = "http://localhost:9001/dex/.well-known/openid-configuration" + )] + pub oidc_configuration_url: String, + + /// OpenID client ID + #[arg(long, env, default_value = "foo")] + pub oidc_client_id: String, + + /// OpenID client secret + #[arg(long, env, default_value = "bar")] + pub oidc_client_secret: String, + + /// OpenID login redirect URL + #[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")] + oidc_redirect_url: String, + + /// S3 Bucket name + #[arg(long, env, default_value = "matrix-gw")] + s3_bucket_name: String, + + /// S3 region (if not using Minio) + #[arg(long, env, default_value = "eu-central-1")] + s3_region: String, + + /// S3 API endpoint + #[arg(long, env, default_value = "http://localhost:9000")] + s3_endpoint: String, + + /// S3 access key + #[arg(long, env, default_value = "minioadmin")] + s3_access_key: String, + + /// S3 secret key + #[arg(long, env, default_value = "minioadmin")] + s3_secret_key: String, + + /// S3 skip auto create bucket if not existing + #[arg(long, env)] + pub s3_skip_auto_create_bucket: bool, +} + +lazy_static::lazy_static! { + static ref ARGS: AppConfig = { + AppConfig::parse() + }; +} + +impl AppConfig { + /// Get parsed command line arguments + pub fn get() -> &'static AppConfig { + &ARGS + } + + /// Get app secret + pub fn secret(&self) -> &str { + let mut secret = self.secret.as_str(); + + if cfg!(debug_assertions) && secret.is_empty() { + secret = "DEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEYDEBUGKEY"; + } + + if secret.is_empty() { + panic!("SECRET is undefined or too short (min 64 chars)!") + } + + secret + } + + /// Get Redis connection configuration + pub fn redis_connection_string(&self) -> String { + format!( + "redis://{}:{}@{}:{}/{}", + self.redis_username.as_deref().unwrap_or(""), + self.redis_password, + self.redis_hostname, + self.redis_port, + self.redis_db_number + ) + } + + /// Get OpenID providers configuration + pub fn openid_provider(&self) -> OIDCProvider<'_> { + OIDCProvider { + client_id: self.oidc_client_id.as_str(), + client_secret: self.oidc_client_secret.as_str(), + configuration_url: self.oidc_configuration_url.as_str(), + redirect_url: self + .oidc_redirect_url + .replace("APP_ORIGIN", &self.website_origin), + } + } + + /// Get s3 bucket credentials + pub fn s3_credentials(&self) -> anyhow::Result { + Ok(Credentials::new( + Some(&self.s3_access_key), + Some(&self.s3_secret_key), + None, + None, + None, + )?) + } + + /// Get S3 bucket + pub fn s3_bucket(&self) -> anyhow::Result> { + Ok(Bucket::new( + &self.s3_bucket_name, + Region::Custom { + region: self.s3_region.to_string(), + endpoint: self.s3_endpoint.to_string(), + }, + self.s3_credentials()?, + )? + .with_path_style()) + } +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct OIDCProvider<'a> { + pub client_id: &'a str, + pub client_secret: &'a str, + pub configuration_url: &'a str, + pub redirect_url: String, +} + +#[cfg(test)] +mod test { + use crate::app_config::AppConfig; + + #[test] + fn verify_cli() { + use clap::CommandFactory; + AppConfig::command().debug_assert() + } +} diff --git a/matrixgw_backend/src/lib.rs b/matrixgw_backend/src/lib.rs new file mode 100644 index 0000000..62a6f82 --- /dev/null +++ b/matrixgw_backend/src/lib.rs @@ -0,0 +1 @@ +pub mod app_config; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index e7a11a9..8bc6c8c 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -1,3 +1,27 @@ -fn main() { - println!("Hello, world!"); +use actix_session::storage::RedisSessionStore; +use actix_web::cookie::Key; +use actix_web::{App, HttpServer}; +use matrixgw_backend::app_config::AppConfig; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let secret_key = Key::from(AppConfig::get().secret().as_bytes()); + + let redis_store = RedisSessionStore::new(AppConfig::get().redis_connection_string()) + .await + .expect("Failed to connect to Redis!"); + + log::info!( + "Starting to listen on {} for {}", + AppConfig::get().listen_address, + AppConfig::get().website_origin + ); + + HttpServer::new(move || App::new()) + .workers(4) + .bind(&AppConfig::get().listen_address)? + .run() + .await } From 830f47b61fe0ddd698f7992cd29d7a4b53cab123 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 3 Nov 2025 19:13:42 +0100 Subject: [PATCH 003/124] Add server config route --- matrixgw_backend/Cargo.lock | 12 +++ matrixgw_backend/Cargo.toml | 3 +- matrixgw_backend/src/app_config.rs | 24 ++++++ matrixgw_backend/src/controllers/mod.rs | 1 + .../src/controllers/server_controller.rs | 74 +++++++++++++++++++ matrixgw_backend/src/lib.rs | 1 + matrixgw_backend/src/main.rs | 33 +++++++-- 7 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 matrixgw_backend/src/controllers/mod.rs create mode 100644 matrixgw_backend/src/controllers/server_controller.rs diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index 079c0e7..a6569f5 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -68,6 +68,17 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-remote-ip" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7629b357d4705cf3f1e31f989f48ecd56027112f7d52dcf06dd96ee197065f8e" +dependencies = [ + "actix-web", + "futures-util", + "log", +] + [[package]] name = "actix-router" version = "0.5.3" @@ -1405,6 +1416,7 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" name = "matrixgw_backend" version = "0.1.0" dependencies = [ + "actix-remote-ip", "actix-session", "actix-web", "anyhow", diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index c282d75..9ae5242 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -13,4 +13,5 @@ serde = { version = "1.0.228", features = ["derive"] } rust-s3 = { version = "0.37.0", features = ["tokio"] } tokio = { version = "1.48.0", features = ["full"] } actix-web = "4.11.0" -actix-session = { version = "0.11.0", features = ["redis-session"] } \ No newline at end of file +actix-session = { version = "0.11.0", features = ["redis-session"] } +actix-remote-ip = "0.1.0" \ No newline at end of file diff --git a/matrixgw_backend/src/app_config.rs b/matrixgw_backend/src/app_config.rs index a1271d7..7552544 100644 --- a/matrixgw_backend/src/app_config.rs +++ b/matrixgw_backend/src/app_config.rs @@ -18,6 +18,11 @@ pub struct AppConfig { #[clap(short, long, env)] pub proxy_ip: Option, + /// Unsecure : for development, bypass authentication, using the account with the given + /// email address by default + #[clap(long, env)] + unsecure_auto_login_email: Option, + /// Secret key, used to secure some resources. Must be randomly generated #[clap(short = 'S', long, env, default_value = "")] secret: String, @@ -54,6 +59,10 @@ pub struct AppConfig { )] pub oidc_configuration_url: String, + /// OpenID provider name + #[arg(long, env, default_value = "3rd party provider")] + pub oidc_provider_name: String, + /// OpenID client ID #[arg(long, env, default_value = "foo")] pub oidc_client_id: String, @@ -103,6 +112,14 @@ impl AppConfig { &ARGS } + /// Get auto login email (if not empty) + pub fn unsecure_auto_login_email(&self) -> Option<&str> { + match self.unsecure_auto_login_email.as_deref() { + None | Some("") => None, + s => s, + } + } + /// Get app secret pub fn secret(&self) -> &str { let mut secret = self.secret.as_str(); @@ -118,6 +135,11 @@ impl AppConfig { secret } + /// Check if auth is disabled + pub fn is_auth_disabled(&self) -> bool { + self.unsecure_auto_login_email().is_some() + } + /// Get Redis connection configuration pub fn redis_connection_string(&self) -> String { format!( @@ -136,6 +158,7 @@ impl AppConfig { client_id: self.oidc_client_id.as_str(), client_secret: self.oidc_client_secret.as_str(), configuration_url: self.oidc_configuration_url.as_str(), + name: self.oidc_provider_name.as_str(), redirect_url: self .oidc_redirect_url .replace("APP_ORIGIN", &self.website_origin), @@ -169,6 +192,7 @@ impl AppConfig { #[derive(Debug, Clone, serde::Serialize)] pub struct OIDCProvider<'a> { + pub name: &'a str, pub client_id: &'a str, pub client_secret: &'a str, pub configuration_url: &'a str, diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs new file mode 100644 index 0000000..98a0949 --- /dev/null +++ b/matrixgw_backend/src/controllers/mod.rs @@ -0,0 +1 @@ +pub mod server_controller; diff --git a/matrixgw_backend/src/controllers/server_controller.rs b/matrixgw_backend/src/controllers/server_controller.rs new file mode 100644 index 0000000..22683ee --- /dev/null +++ b/matrixgw_backend/src/controllers/server_controller.rs @@ -0,0 +1,74 @@ +use crate::app_config::AppConfig; +use actix_web::HttpResponse; + +/// Serve robots.txt (disallow ranking) +pub async fn robots_txt() -> HttpResponse { + HttpResponse::Ok() + .content_type("text/plain") + .body("User-agent: *\nDisallow: /\n") +} + +#[derive(serde::Serialize)] +pub struct LenConstraints { + min: usize, + max: usize, +} + +impl LenConstraints { + pub fn new(min: usize, max: usize) -> Self { + Self { min, max } + } + pub fn not_empty(max: usize) -> Self { + Self { min: 1, max } + } + pub fn max_only(max: usize) -> Self { + Self { min: 0, max } + } + + pub fn check_str(&self, s: &str) -> bool { + s.len() >= self.min && s.len() <= self.max + } + + pub fn check_u32(&self, v: u32) -> bool { + v >= self.min as u32 && v <= self.max as u32 + } +} + +#[derive(serde::Serialize)] +pub struct ServerConstraints { + pub token_name: LenConstraints, + pub token_ip_net: LenConstraints, + pub token_max_inactivity: LenConstraints, +} + +impl Default for ServerConstraints { + fn default() -> Self { + Self { + token_name: LenConstraints::new(5, 255), + token_ip_net: LenConstraints::max_only(44), + token_max_inactivity: LenConstraints::new(3600, 3600 * 24 * 365), + } + } +} + +#[derive(serde::Serialize)] +struct ServerConfig { + auth_disabled: bool, + oidc_provider_name: &'static str, + constraints: ServerConstraints, +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + auth_disabled: AppConfig::get().is_auth_disabled(), + oidc_provider_name: AppConfig::get().openid_provider().name, + constraints: Default::default(), + } + } +} + +/// Get server static configuration +pub async fn config() -> HttpResponse { + HttpResponse::Ok().json(ServerConfig::default()) +} diff --git a/matrixgw_backend/src/lib.rs b/matrixgw_backend/src/lib.rs index 62a6f82..2483671 100644 --- a/matrixgw_backend/src/lib.rs +++ b/matrixgw_backend/src/lib.rs @@ -1 +1,2 @@ pub mod app_config; +pub mod controllers; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 8bc6c8c..3d6132f 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -1,7 +1,11 @@ +use actix_remote_ip::RemoteIPConfig; +use actix_session::SessionMiddleware; +use actix_session::config::SessionLifecycle; use actix_session::storage::RedisSessionStore; use actix_web::cookie::Key; -use actix_web::{App, HttpServer}; +use actix_web::{App, HttpServer, web}; use matrixgw_backend::app_config::AppConfig; +use matrixgw_backend::controllers::server_controller; #[tokio::main] async fn main() -> std::io::Result<()> { @@ -19,9 +23,26 @@ async fn main() -> std::io::Result<()> { AppConfig::get().website_origin ); - HttpServer::new(move || App::new()) - .workers(4) - .bind(&AppConfig::get().listen_address)? - .run() - .await + HttpServer::new(move || { + App::new() + .wrap( + SessionMiddleware::builder(redis_store.clone(), secret_key.clone()) + .cookie_name("matrixgw-session".to_string()) + .session_lifecycle(SessionLifecycle::BrowserSession(Default::default())) + .build(), + ) + .app_data(web::Data::new(RemoteIPConfig { + proxy: AppConfig::get().proxy_ip.clone(), + })) + // Server controller + .route("/robots.txt", web::get().to(server_controller::robots_txt)) + .route( + "/api/server/config", + web::get().to(server_controller::config), + ) + }) + .workers(4) + .bind(&AppConfig::get().listen_address)? + .run() + .await } From bc815a5cf16f8cef84041078cf5ef85514e7942f Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 3 Nov 2025 22:17:29 +0100 Subject: [PATCH 004/124] Add users authentication routes --- README.md | 4 +- matrixgw_backend/.gitignore | 1 + matrixgw_backend/Cargo.lock | 948 +++++++++++------- matrixgw_backend/Cargo.toml | 16 +- matrixgw_backend/docker-compose.yml | 13 - matrixgw_backend/docker/dex/dex.config.yaml | 2 +- matrixgw_backend/examples/api_curl.rs | 9 +- matrixgw_backend/src/app_config.rs | 78 +- matrixgw_backend/src/constants.rs | 15 + .../src/controllers/auth_controller.rs | 131 +++ matrixgw_backend/src/controllers/mod.rs | 33 + .../src/extractors/auth_extractor.rs | 305 ++++++ matrixgw_backend/src/extractors/mod.rs | 2 + .../src/extractors/session_extractor.rs | 91 ++ matrixgw_backend/src/lib.rs | 4 + matrixgw_backend/src/main.rs | 24 +- matrixgw_backend/src/users.rs | 168 ++++ matrixgw_backend/src/utils/crypt_utils.rs | 6 + matrixgw_backend/src/utils/mod.rs | 3 + matrixgw_backend/src/utils/rand_utils.rs | 6 + matrixgw_backend/src/utils/time_utils.rs | 9 + 21 files changed, 1417 insertions(+), 451 deletions(-) create mode 100644 matrixgw_backend/src/constants.rs create mode 100644 matrixgw_backend/src/controllers/auth_controller.rs create mode 100644 matrixgw_backend/src/extractors/auth_extractor.rs create mode 100644 matrixgw_backend/src/extractors/mod.rs create mode 100644 matrixgw_backend/src/extractors/session_extractor.rs create mode 100644 matrixgw_backend/src/users.rs create mode 100644 matrixgw_backend/src/utils/crypt_utils.rs create mode 100644 matrixgw_backend/src/utils/mod.rs create mode 100644 matrixgw_backend/src/utils/rand_utils.rs create mode 100644 matrixgw_backend/src/utils/time_utils.rs diff --git a/README.md b/README.md index 8cc8aa6..0c7620e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ docker run --rm -it docker.io/pierre42100/matrix_gateway --help ### Dependencies ``` cd matrixgw_backend -mkdir -p storage/maspostgres storage/synapse storage/minio +mkdir -p storage/maspostgres storage/synapse docker compose up ``` @@ -35,14 +35,12 @@ URLs: * Synapse: http://localhost:8448/ * Matrix Authentication Service: http://localhost:8778/ * OpenID configuration: http://127.0.0.1:9001/dex/.well-known/openid-configuration -* Minio console: http://localhost:9002/ Auto-created Matrix accounts: * `admin1` : `admin1` * `user1` : `user1` -Minio administration credentials: `minioadmin` : `minioadmin` ### Backend ```bash diff --git a/matrixgw_backend/.gitignore b/matrixgw_backend/.gitignore index 79e4ba8..f3a3b05 100644 --- a/matrixgw_backend/.gitignore +++ b/matrixgw_backend/.gitignore @@ -1,3 +1,4 @@ storage +app_storage .idea target diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index a6569f5..ad5c8da 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -39,7 +39,7 @@ dependencies = [ "flate2", "foldhash", "futures-core", - "h2", + "h2 0.3.27", "http 0.2.12", "httparse", "httpdate", @@ -342,15 +342,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] -name = "async-trait" -version = "0.1.89" +name = "arrayref" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" [[package]] name = "atomic-waker" @@ -358,53 +365,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "attohttpc" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" -dependencies = [ - "base64 0.22.1", - "http 1.3.1", - "log", - "native-tls", - "serde", - "serde_json", - "url", -] - [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-creds" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13804829a843b3f26e151c97acbb315ee1177a2724690edfcd28f1894146200" -dependencies = [ - "attohttpc", - "home", - "log", - "quick-xml", - "rust-ini", - "serde", - "thiserror", - "time", - "url", -] - -[[package]] -name = "aws-region" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5532f65342f789f9c1b7078ea9c9cd9293cd62dcc284fa99adc4a1c9ba43469c" -dependencies = [ - "thiserror", -] - [[package]] name = "backon" version = "1.6.0" @@ -414,6 +380,18 @@ dependencies = [ "fastrand", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base16ct" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b" + [[package]] name = "base64" version = "0.20.0" @@ -426,12 +404,35 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -483,15 +484,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.2.44" @@ -560,6 +552,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "coarsetime" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -581,37 +584,16 @@ dependencies = [ ] [[package]] -name = "compact_str" -version = "0.7.1" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "ryu", - "static_assertions", -] +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "const-random" -version = "0.1.18" +name = "constant_time_eq" +version = "0.3.1" 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 0.2.16", - "once_cell", - "tiny-keccak", -] +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "cookie" @@ -666,10 +648,16 @@ dependencies = [ ] [[package]] -name = "crunchy" -version = "0.2.4" +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] [[package]] name = "crypto-common" @@ -682,6 +670,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "ct-codecs" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" + [[package]] name = "ctr" version = "0.9.2" @@ -691,6 +685,17 @@ dependencies = [ "cipher", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -698,7 +703,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde_core", ] [[package]] @@ -729,6 +733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -745,12 +750,48 @@ dependencies = [ ] [[package]] -name = "dlv-list" -version = "0.5.2" +name = "ecdsa" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "const-random", + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom 0.2.16", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", ] [[package]] @@ -801,12 +842,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -874,12 +934,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - [[package]] name = "futures-macro" version = "0.3.31" @@ -910,11 +964,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", - "futures-io", "futures-macro", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", "slab", @@ -928,6 +980,7 @@ checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -937,8 +990,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -963,6 +1018,17 @@ dependencies = [ "polyval", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.27" @@ -983,10 +1049,23 @@ dependencies = [ ] [[package]] -name = "hashbrown" -version = "0.14.5" +name = "h2" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] [[package]] name = "hashbrown" @@ -1025,12 +1104,27 @@ dependencies = [ ] [[package]] -name = "home" -version = "0.5.12" +name = "hmac-sha1-compact" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3" + +[[package]] +name = "hmac-sha256" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" dependencies = [ - "windows-sys 0.61.2", + "digest", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280" +dependencies = [ + "digest", ] [[package]] @@ -1100,6 +1194,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2 0.4.12", "http 1.3.1", "http-body", "httparse", @@ -1111,6 +1206,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1146,9 +1257,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.1", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1266,7 +1379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown", ] [[package]] @@ -1283,6 +1396,9 @@ name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -1350,6 +1466,46 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwt-simple" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad8761f175784dfbb83709f322fc4daf6b27afd5bf375492f2876f9e925ef5a" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "serde", + "serde_json", + "superboring", + "thiserror", + "zeroize", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1361,6 +1517,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1368,6 +1527,26 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "light-openid" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a15777d080e807d5b6b3c0b5a293f7d4680d74a4c66b0cdf9db0441ea9f548" +dependencies = [ + "base64 0.22.1", + "log", + "reqwest", + "serde", + "serde_json", + "urlencoding", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1412,6 +1591,16 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "mailchecker" +version = "6.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abad4bc63045f04cfc55aa4c55d4ec0a890c377ce56463bfc2adc2bc059c4b84" +dependencies = [ + "fast_chemail", + "once_cell", +] + [[package]] name = "matrixgw_backend" version = "0.1.0" @@ -1420,32 +1609,27 @@ dependencies = [ "actix-session", "actix-web", "anyhow", + "base16ct 0.3.0", + "bytes", "clap", "env_logger", + "futures-util", + "hex", + "ipnet", + "jwt-simple", "lazy_static", + "light-openid", "log", - "rust-s3", + "mailchecker", + "rand 0.9.2", "serde", + "sha2", + "thiserror", "tokio", + "urlencoding", + "uuid", ] -[[package]] -name = "maybe-async" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "md5" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" - [[package]] name = "memchr" version = "2.7.6" @@ -1458,15 +1642,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minidom" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e394a0e3c7ccc2daea3dffabe82f09857b6b510cb25af87d54bf3e910ac1642d" -dependencies = [ - "rxml", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1506,15 +1681,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ntapi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" -dependencies = [ - "winapi", -] - [[package]] name = "num-bigint" version = "0.4.6" @@ -1525,6 +1691,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1540,6 +1722,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1547,25 +1740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", + "libm", ] [[package]] @@ -1631,13 +1806,27 @@ dependencies = [ ] [[package]] -name = "ordered-multimap" -version = "0.7.3" +name = "p256" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "dlv-list", - "hashbrown 0.14.5", + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", ] [[package]] @@ -1663,6 +1852,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1681,6 +1879,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1738,6 +1957,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -1747,16 +1975,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-xml" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quote" version = "1.0.41" @@ -1907,16 +2125,19 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", - "futures-util", + "h2 0.4.12", "http 1.3.1", "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "js-sys", "log", + "mime", "native-tls", "percent-encoding", "pin-project-lite", @@ -1927,60 +2148,58 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", ] [[package]] -name = "rust-ini" -version = "0.21.3" +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "cfg-if", - "ordered-multimap", + "hmac", + "subtle", ] [[package]] -name = "rust-s3" -version = "0.37.0" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f9b973bd4097f5bb47e5827dcb9fb5dc17e93879e46badc27d2a4e9a4e5588" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "async-trait", - "aws-creds", - "aws-region", - "base64 0.22.1", - "bytes", + "cc", "cfg-if", - "futures-util", - "hex", - "hmac", - "http 1.3.1", - "log", - "maybe-async", - "md5", - "minidom", - "percent-encoding", - "quick-xml", - "reqwest", - "serde", - "serde_derive", - "serde_json", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", "sha2", - "sysinfo", - "thiserror", - "time", - "tokio", - "tokio-stream", - "url", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] @@ -1996,6 +2215,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pki-types" version = "1.13.0" @@ -2005,31 +2237,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "rxml" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc94b580d0f5a6b7a2d604e597513d3c673154b52ddeccd1d5c32360d945ee" -dependencies = [ - "bytes", - "rxml_validation", -] - -[[package]] -name = "rxml_validation" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826e80413b9a35e9d33217b3dcac04cf95f6559d15944b93887a08be5496c4a4" -dependencies = [ - "compact_str", -] - [[package]] name = "ryu" version = "1.0.20" @@ -2051,6 +2275,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -2166,6 +2404,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2204,18 +2452,28 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" @@ -2228,6 +2486,19 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "superboring" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" +dependencies = [ + "getrandom 0.2.16", + "hmac-sha256", + "hmac-sha512", + "rand 0.8.5", + "rsa", +] + [[package]] name = "syn" version = "2.0.108" @@ -2260,17 +2531,24 @@ dependencies = [ ] [[package]] -name = "sysinfo" -version = "0.37.2" +name = "system-configuration" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows", ] [[package]] @@ -2337,15 +2615,6 @@ 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 = "tinystr" version = "0.8.2" @@ -2395,13 +2664,12 @@ dependencies = [ ] [[package]] -name = "tokio-stream" -version = "0.1.17" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "futures-core", - "pin-project-lite", + "rustls", "tokio", ] @@ -2529,6 +2797,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.7" @@ -2541,6 +2815,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2553,6 +2833,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2589,6 +2881,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi", +] + [[package]] name = "wasm-bindgen" version = "0.2.105" @@ -2647,19 +2948,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "web-sys" version = "0.3.82" @@ -2670,96 +2958,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.1.3" @@ -2773,13 +2971,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-numerics" -version = "0.2.0" +name = "windows-registry" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-core", "windows-link 0.1.3", + "windows-result", + "windows-strings", ] [[package]] @@ -2860,15 +3059,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index 9ae5242..dfb0d9e 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -10,8 +10,20 @@ clap = { version = "4.5.51", features = ["derive", "env"] } lazy_static = "1.5.0" anyhow = "1.0.100" serde = { version = "1.0.228", features = ["derive"] } -rust-s3 = { version = "0.37.0", features = ["tokio"] } tokio = { version = "1.48.0", features = ["full"] } actix-web = "4.11.0" actix-session = { version = "0.11.0", features = ["redis-session"] } -actix-remote-ip = "0.1.0" \ No newline at end of file +actix-remote-ip = "0.1.0" +light-openid = "1.0.4" +bytes = "1.10.1" +sha2 = "0.10.9" +urlencoding = "2.1.3" +base16ct = { version = "0.3.0", features = ["alloc"] } +futures-util = "0.3.31" +jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] } +thiserror = "2.0.17" +uuid = { version = "1.18.1", features = ["v4", "serde"] } +ipnet = { version = "2.11.0", features = ["serde"] } +rand = "0.9.2" +hex = "0.4.3" +mailchecker = "6.0.19" \ No newline at end of file diff --git a/matrixgw_backend/docker-compose.yml b/matrixgw_backend/docker-compose.yml index fec7c62..be4a7bb 100644 --- a/matrixgw_backend/docker-compose.yml +++ b/matrixgw_backend/docker-compose.yml @@ -92,19 +92,6 @@ services: - ./docker/dex:/conf:ro command: [ "dex", "serve", "/conf/dex.config.yaml" ] - minio: - image: quay.io/minio/minio - command: minio server --console-address ":9002" /data - ports: - - 9000:9000/tcp - - 9002:9002/tcp - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin - volumes: - # You may store the database tables in a local folder.. - - ./storage/minio:/data - redis: image: redis:alpine command: redis-server --requirepass ${REDIS_PASS:-secretredis} diff --git a/matrixgw_backend/docker/dex/dex.config.yaml b/matrixgw_backend/docker/dex/dex.config.yaml index 7704800..0b9d381 100644 --- a/matrixgw_backend/docker/dex/dex.config.yaml +++ b/matrixgw_backend/docker/dex/dex.config.yaml @@ -22,5 +22,5 @@ staticClients: - id: foo secret: bar redirectURIs: - - http://localhost:8000/oidc_cb + - http://localhost:5173/oidc_cb name: Project diff --git a/matrixgw_backend/examples/api_curl.rs b/matrixgw_backend/examples/api_curl.rs index a2669fd..eb57533 100644 --- a/matrixgw_backend/examples/api_curl.rs +++ b/matrixgw_backend/examples/api_curl.rs @@ -1,11 +1,12 @@ use clap::Parser; use jwt_simple::algorithms::HS256Key; use jwt_simple::prelude::{Clock, Duration, JWTClaims, MACLike}; -use matrix_gateway::extractors::client_auth::TokenClaims; -use matrix_gateway::utils::base_utils::rand_str; +use matrixgw_backend::constants; +use matrixgw_backend::extractors::auth_extractor::TokenClaims; use std::ops::Add; use std::os::unix::prelude::CommandExt; use std::process::Command; +use matrixgw_backend::utils::rand_utils::rand_string; /// cURL wrapper to query MatrixGW #[derive(Parser, Debug)] @@ -59,7 +60,7 @@ fn main() { subject: None, audiences: None, jwt_id: None, - nonce: Some(rand_str(10)), + nonce: Some(rand_string(10)), custom: TokenClaims { method: args.method.to_string(), uri: args.uri, @@ -78,7 +79,7 @@ fn main() { let _ = Command::new("curl") .args(["-X", &args.method]) - .args(["-H", &format!("x-client-auth: {jwt}")]) + .args(["-H", &format!("{}: {jwt}", constants::API_AUTH_HEADER)]) .args(args.run) .arg(full_url) .exec(); diff --git a/matrixgw_backend/src/app_config.rs b/matrixgw_backend/src/app_config.rs index 7552544..ed122d8 100644 --- a/matrixgw_backend/src/app_config.rs +++ b/matrixgw_backend/src/app_config.rs @@ -1,6 +1,7 @@ +use crate::users::{APITokenID, UserEmail}; +use crate::utils::crypt_utils::sha256str; use clap::Parser; -use s3::creds::Credentials; -use s3::{Bucket, Region}; +use std::path::{Path, PathBuf}; /// Matrix gateway backend API #[derive(Parser, Debug, Clone)] @@ -11,7 +12,7 @@ pub struct AppConfig { pub listen_address: String, /// Website origin - #[clap(short, long, env, default_value = "http://localhost:8000")] + #[clap(short, long, env, default_value = "http://localhost:5173")] pub website_origin: String, /// Proxy IP, might end with a star "*" @@ -75,29 +76,9 @@ pub struct AppConfig { #[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")] oidc_redirect_url: String, - /// S3 Bucket name - #[arg(long, env, default_value = "matrix-gw")] - s3_bucket_name: String, - - /// S3 region (if not using Minio) - #[arg(long, env, default_value = "eu-central-1")] - s3_region: String, - - /// S3 API endpoint - #[arg(long, env, default_value = "http://localhost:9000")] - s3_endpoint: String, - - /// S3 access key - #[arg(long, env, default_value = "minioadmin")] - s3_access_key: String, - - /// S3 secret key - #[arg(long, env, default_value = "minioadmin")] - s3_secret_key: String, - - /// S3 skip auto create bucket if not existing - #[arg(long, env)] - pub s3_skip_auto_create_bucket: bool, + /// Application storage path + #[arg(long, env, default_value = "app_storage")] + storage_path: String, } lazy_static::lazy_static! { @@ -113,10 +94,10 @@ impl AppConfig { } /// Get auto login email (if not empty) - pub fn unsecure_auto_login_email(&self) -> Option<&str> { + pub fn unsecure_auto_login_email(&self) -> Option { match self.unsecure_auto_login_email.as_deref() { None | Some("") => None, - s => s, + Some(s) => Some(UserEmail(s.to_owned())), } } @@ -165,28 +146,29 @@ impl AppConfig { } } - /// Get s3 bucket credentials - pub fn s3_credentials(&self) -> anyhow::Result { - Ok(Credentials::new( - Some(&self.s3_access_key), - Some(&self.s3_secret_key), - None, - None, - None, - )?) + /// Get storage path + pub fn storage_path(&self) -> &Path { + Path::new(self.storage_path.as_str()) } - /// Get S3 bucket - pub fn s3_bucket(&self) -> anyhow::Result> { - Ok(Bucket::new( - &self.s3_bucket_name, - Region::Custom { - region: self.s3_region.to_string(), - endpoint: self.s3_endpoint.to_string(), - }, - self.s3_credentials()?, - )? - .with_path_style()) + /// User storage directory + pub fn user_directory(&self, mail: &UserEmail) -> PathBuf { + self.storage_path().join("users").join(sha256str(&mail.0)) + } + + /// User metadata file + pub fn user_metadata_file_path(&self, mail: &UserEmail) -> PathBuf { + self.user_directory(mail).join("metadata.json") + } + + /// User API tokens directory + pub fn user_api_token_directory(&self, mail: &UserEmail) -> PathBuf { + self.user_directory(mail).join("api-tokens") + } + + /// User API token metadata file + pub fn user_api_token_metadata_file(&self, mail: &UserEmail, id: &APITokenID) -> PathBuf { + self.user_api_token_directory(mail).join(id.0.to_string()) } } diff --git a/matrixgw_backend/src/constants.rs b/matrixgw_backend/src/constants.rs new file mode 100644 index 0000000..d46042c --- /dev/null +++ b/matrixgw_backend/src/constants.rs @@ -0,0 +1,15 @@ +/// Auth header +pub const API_AUTH_HEADER: &str = "x-client-auth"; + +/// Max token validity, in seconds +pub const API_TOKEN_JWT_MAX_DURATION: u64 = 15 * 60; + +/// Session-specific constants +pub mod sessions { + /// OpenID auth session state key + pub const OIDC_STATE_KEY: &str = "oidc-state"; + /// OpenID auth remote IP address + pub const OIDC_REMOTE_IP: &str = "oidc-remote-ip"; + /// Authenticated ID + pub const USER_ID: &str = "uid"; +} diff --git a/matrixgw_backend/src/controllers/auth_controller.rs b/matrixgw_backend/src/controllers/auth_controller.rs new file mode 100644 index 0000000..ab14ace --- /dev/null +++ b/matrixgw_backend/src/controllers/auth_controller.rs @@ -0,0 +1,131 @@ +use crate::app_config::AppConfig; +use crate::controllers::{HttpFailure, HttpResult}; +use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; +use crate::extractors::session_extractor::MatrixGWSession; +use crate::users::{User, UserEmail}; +use actix_remote_ip::RemoteIP; +use actix_web::{HttpResponse, web}; +use light_openid::primitives::OpenIDConfig; + +#[derive(serde::Serialize)] +struct StartOIDCResponse { + url: String, +} + +/// Start OIDC authentication +pub async fn start_oidc(session: MatrixGWSession, remote_ip: RemoteIP) -> HttpResult { + let prov = AppConfig::get().openid_provider(); + + let conf = match OpenIDConfig::load_from_url(prov.configuration_url).await { + Ok(c) => c, + Err(e) => { + log::error!("Failed to fetch OpenID provider configuration! {e}"); + return Ok(HttpResponse::InternalServerError() + .json("Failed to fetch OpenID provider configuration!")); + } + }; + + let state = match session.gen_oidc_state(remote_ip.0) { + Ok(s) => s, + Err(e) => { + log::error!("Failed to generate auth state! {e}"); + return Ok(HttpResponse::InternalServerError().json("Failed to generate auth state!")); + } + }; + + Ok(HttpResponse::Ok().json(StartOIDCResponse { + url: conf.gen_authorization_url( + prov.client_id, + &state, + &AppConfig::get().openid_provider().redirect_url, + ), + })) +} + +#[derive(serde::Deserialize)] +pub struct FinishOpenIDLoginQuery { + code: String, + state: String, +} + +/// Finish OIDC authentication +pub async fn finish_oidc( + session: MatrixGWSession, + remote_ip: RemoteIP, + req: web::Json, +) -> HttpResult { + if let Err(e) = session.validate_state(&req.state, remote_ip.0) { + log::error!("Failed to validate OIDC CB state! {e}"); + return Ok(HttpResponse::BadRequest().json("Invalid state!")); + } + + let prov = AppConfig::get().openid_provider(); + + let conf = OpenIDConfig::load_from_url(prov.configuration_url) + .await + .map_err(HttpFailure::OpenID)?; + + let (token, _) = conf + .request_token( + prov.client_id, + prov.client_secret, + &req.code, + &AppConfig::get().openid_provider().redirect_url, + ) + .await + .map_err(HttpFailure::OpenID)?; + let (user_info, _) = conf + .request_user_info(&token) + .await + .map_err(HttpFailure::OpenID)?; + + if user_info.email_verified != Some(true) { + log::error!("Email is not verified!"); + return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!")); + } + + let mail = match user_info.email { + Some(m) => m, + None => { + return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!")); + } + }; + + let user_name = user_info.name.unwrap_or_else(|| { + format!( + "{} {}", + user_info.given_name.as_deref().unwrap_or(""), + user_info.family_name.as_deref().unwrap_or("") + ) + }); + + let user = User::create_or_update_user(&UserEmail(mail), &user_name).await?; + + session.set_user(&user)?; + + Ok(HttpResponse::Ok().finish()) +} + +/// Get current user information +pub async fn auth_info(auth: AuthExtractor) -> HttpResult { + Ok(HttpResponse::Ok().json(auth.user)) +} + +/// Sign out user +pub async fn sign_out(auth: AuthExtractor, session: MatrixGWSession) -> HttpResult { + match auth.method { + AuthenticatedMethod::Cookie => { + session.unset_current_user()?; + } + + AuthenticatedMethod::Token(token) => { + token.delete(&auth.user.email).await?; + } + + AuthenticatedMethod::Dev => { + // Nothing to be done, user is always authenticated + } + } + + Ok(HttpResponse::NoContent().finish()) +} diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 98a0949..25104bf 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -1 +1,34 @@ +use actix_web::http::StatusCode; +use actix_web::{HttpResponse, ResponseError}; +use std::error::Error; + +pub mod auth_controller; pub mod server_controller; + +#[derive(thiserror::Error, Debug)] +pub enum HttpFailure { + #[error("this resource requires higher privileges")] + Forbidden, + #[error("this resource was not found")] + NotFound, + #[error("an unspecified open id error occurred: {0}")] + OpenID(Box), + #[error("an unspecified internal error occurred: {0}")] + InternalError(#[from] anyhow::Error), +} + +impl ResponseError for HttpFailure { + fn status_code(&self) -> StatusCode { + match &self { + Self::Forbidden => StatusCode::FORBIDDEN, + Self::NotFound => StatusCode::NOT_FOUND, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::build(self.status_code()).body(self.to_string()) + } +} + +pub type HttpResult = Result; diff --git a/matrixgw_backend/src/extractors/auth_extractor.rs b/matrixgw_backend/src/extractors/auth_extractor.rs new file mode 100644 index 0000000..8c75a29 --- /dev/null +++ b/matrixgw_backend/src/extractors/auth_extractor.rs @@ -0,0 +1,305 @@ +use crate::app_config::AppConfig; +use crate::constants; +use crate::extractors::session_extractor::MatrixGWSession; +use crate::users::{APIToken, APITokenID, User, UserEmail}; +use crate::utils::time_utils::time_secs; +use actix_remote_ip::RemoteIP; +use actix_web::dev::Payload; +use actix_web::error::ErrorPreconditionFailed; +use actix_web::{FromRequest, HttpRequest}; +use anyhow::Context; +use bytes::Bytes; +use jwt_simple::common::VerificationOptions; +use jwt_simple::prelude::{Duration, HS256Key, MACLike}; +use sha2::{Digest, Sha256}; +use std::fmt::Display; +use std::net::IpAddr; +use std::str::FromStr; + +#[derive(Debug, Clone)] +pub enum AuthenticatedMethod { + /// User is authenticated using a cookie + Cookie, + /// User is authenticated through command line, for debugging purposes only + Dev, + /// User is authenticated using an API token + Token(APIToken), +} + +pub struct AuthExtractor { + pub user: User, + pub method: AuthenticatedMethod, + pub payload: Option>, +} + +#[derive(Debug, Eq, PartialEq)] +pub struct MatrixJWTKID { + pub user_email: UserEmail, + pub id: APITokenID, +} + +impl Display for MatrixJWTKID { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}#{}", self.user_email.0, self.id.0) + } +} + +impl FromStr for MatrixJWTKID { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let (mail, token_id) = s + .split_once("#") + .context("Failed to decode KID in two parts!")?; + + let mail = UserEmail(mail.to_string()); + + if !mail.is_valid() { + anyhow::bail!("Given email is invalid!") + } + + Ok(Self { + user_email: mail, + id: token_id.parse().context("Failed to parse API token ID")?, + }) + } +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct TokenClaims { + #[serde(rename = "met")] + pub method: String, + pub uri: String, + #[serde(rename = "pay", skip_serializing_if = "Option::is_none")] + pub payload_sha256: Option, +} + +impl AuthExtractor { + async fn extract_auth( + req: &HttpRequest, + remote_ip: IpAddr, + payload_bytes: Option, + ) -> Result { + // Check for authentication using API token + if let Some(token) = req.headers().get(constants::API_AUTH_HEADER) { + let Ok(jwt_token) = token.to_str() else { + return Err(actix_web::error::ErrorBadRequest( + "Failed to decode token as string!", + )); + }; + + let metadata = match jwt_simple::token::Token::decode_metadata(jwt_token) { + Ok(m) => m, + Err(e) => { + log::error!("Failed to decode JWT header metadata! {e}"); + return Err(actix_web::error::ErrorBadRequest( + "Failed to decode JWT header metadata!", + )); + } + }; + + // Extract token ID + let Some(kid) = metadata.key_id() else { + return Err(actix_web::error::ErrorBadRequest( + "Missing key id in request!", + )); + }; + + let jwt_kid = match MatrixJWTKID::from_str(kid) { + Ok(i) => i, + Err(e) => { + log::error!("Failed to parse token id! {e}"); + return Err(actix_web::error::ErrorBadRequest( + "Failed to parse token id!", + )); + } + }; + + // Get token information + let Ok(mut token) = APIToken::load(&jwt_kid.user_email, &jwt_kid.id).await else { + log::error!("Token not found!"); + return Err(actix_web::error::ErrorForbidden("Token not found!")); + }; + + // Decode JWT + let key = HS256Key::from_bytes(token.secret.as_ref()); + let verif = VerificationOptions { + max_validity: Some(Duration::from_secs(constants::API_TOKEN_JWT_MAX_DURATION)), + ..Default::default() + }; + + let claims = match key.verify_token::(jwt_token, Some(verif)) { + Ok(t) => t, + Err(e) => { + log::error!("JWT validation failed! {e}"); + return Err(actix_web::error::ErrorForbidden("JWT validation failed!")); + } + }; + + // Check for nonce + if claims.nonce.is_none() { + return Err(actix_web::error::ErrorBadRequest( + "A nonce is required in auth JWT!", + )); + } + + // Check IP restriction + if let Some(net) = token.network + && !net.contains(&remote_ip) + { + log::error!( + "Trying to use token {:?} from unauthorized IP address: {remote_ip:?}", + token.id + ); + return Err(actix_web::error::ErrorForbidden( + "This token cannot be used from this IP address!", + )); + } + + // Check for write access + if token.read_only && !req.method().is_safe() { + return Err(actix_web::error::ErrorBadRequest( + "Read only token cannot perform write operations!", + )); + } + + // Get user information + let Ok(user) = User::get_by_mail(&jwt_kid.user_email).await else { + return Err(actix_web::error::ErrorBadRequest( + "Failed to get user information from token!", + )); + }; + + // Update last use (if needed) + if token.shall_update_time_used() { + token.last_used = time_secs(); + if let Err(e) = token.write(&jwt_kid.user_email).await { + log::error!("Failed to refresh last usage of token! {e}"); + } + } + + // Handle tokens expiration + if token.is_expired() { + log::error!("Attempted to use expired token! {token:?}"); + return Err(actix_web::error::ErrorBadRequest("Token has expired!")); + } + + // Check payload + let payload = match (payload_bytes, claims.custom.payload_sha256) { + (None, _) => None, + (Some(_), None) => { + return Err(actix_web::error::ErrorBadRequest( + "A payload digest must be included in the JWT when the request has a payload!", + )); + } + (Some(payload), Some(provided_digest)) => { + let computed_digest = base16ct::lower::encode_string(&Sha256::digest(&payload)); + if computed_digest != provided_digest { + log::error!( + "Expected digest {provided_digest} for payload but computed {computed_digest}!" + ); + return Err(actix_web::error::ErrorBadRequest( + "Computed digest is different from the one provided in the JWT!", + )); + } + + Some(payload.to_vec()) + } + }; + + return Ok(Self { + method: AuthenticatedMethod::Token(token), + user, + payload, + }); + } + + // Check if login is hard-coded as program argument + if let Some(email) = &AppConfig::get().unsecure_auto_login_email() { + let user = User::get_by_mail(email).await.map_err(|e| { + log::error!("Failed to retrieve dev user: {e}"); + ErrorPreconditionFailed("Unable to retrieve dev user!") + })?; + return Ok(Self { + method: AuthenticatedMethod::Dev, + user, + payload: payload_bytes.map(|bytes| bytes.to_vec()), + }); + } + + // Check for cookie authentication + let session = MatrixGWSession::extract(req).await?; + if let Some(mail) = session.current_user().map_err(|e| { + log::error!("Failed to retrieve user id: {e}"); + ErrorPreconditionFailed("Failed to read session information!") + })? { + let user = User::get_by_mail(&mail).await.map_err(|e| { + log::error!("Failed to retrieve user from cookie session: {e}"); + ErrorPreconditionFailed("Failed to retrieve user information!") + })?; + return Ok(Self { + method: AuthenticatedMethod::Cookie, + user, + payload: payload_bytes.map(|bytes| bytes.to_vec()), + }); + }; + + Err(ErrorPreconditionFailed("Authentication required!")) + } +} + +impl FromRequest for AuthExtractor { + type Error = actix_web::Error; + type Future = futures_util::future::LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let req = req.clone(); + + let remote_ip = match RemoteIP::from_request(&req, &mut Payload::None).into_inner() { + Ok(ip) => ip, + Err(e) => return Box::pin(async { Err(e) }), + }; + + let mut payload = payload.take(); + + Box::pin(async move { + let payload_bytes = match Bytes::from_request(&req, &mut payload).await { + Ok(b) => { + if b.is_empty() { + None + } else { + Some(b) + } + } + Err(e) => { + log::error!("Failed to extract request payload! {e}"); + None + } + }; + + Self::extract_auth(&req, remote_ip.0, payload_bytes).await + }) + } +} + +#[cfg(test)] +mod tests { + use crate::extractors::auth_extractor::MatrixJWTKID; + use crate::users::{APITokenID, UserEmail}; + use std::str::FromStr; + + #[test] + fn encode_decode_jwt_kid() { + let src = MatrixJWTKID { + user_email: UserEmail("test@mail.com".to_string()), + id: APITokenID::default(), + }; + let encoded = src.to_string(); + let decoded = encoded.parse::().unwrap(); + assert_eq!(src, decoded); + + MatrixJWTKID::from_str("bad").unwrap_err(); + MatrixJWTKID::from_str("ba#d").unwrap_err(); + MatrixJWTKID::from_str("test@valid.com#d").unwrap_err(); + } +} diff --git a/matrixgw_backend/src/extractors/mod.rs b/matrixgw_backend/src/extractors/mod.rs new file mode 100644 index 0000000..4e4f77f --- /dev/null +++ b/matrixgw_backend/src/extractors/mod.rs @@ -0,0 +1,2 @@ +pub mod auth_extractor; +pub mod session_extractor; diff --git a/matrixgw_backend/src/extractors/session_extractor.rs b/matrixgw_backend/src/extractors/session_extractor.rs new file mode 100644 index 0000000..b33dc59 --- /dev/null +++ b/matrixgw_backend/src/extractors/session_extractor.rs @@ -0,0 +1,91 @@ +use crate::constants; +use crate::users::{User, UserEmail}; +use crate::utils::rand_utils::rand_string; +use actix_session::Session; +use actix_web::dev::Payload; +use actix_web::{Error, FromRequest, HttpRequest}; +use futures_util::future::{Ready, ready}; +use std::net::IpAddr; + +/// Matrix Gateway session errors +#[derive(thiserror::Error, Debug)] +enum MatrixGWSessionError { + #[error("Missing state!")] + OIDCMissingState, + #[error("Missing IP address!")] + OIDCMissingIP, + #[error("Invalid state!")] + OIDCInvalidState, + #[error("Invalid IP address!")] + OIDCInvalidIP, +} + +/// Matrix Gateway session +/// +/// Basic wrapper around actix-session extractor +pub struct MatrixGWSession(Session); + +impl MatrixGWSession { + /// Generate OpenID state for this session + pub fn gen_oidc_state(&self, ip: IpAddr) -> anyhow::Result { + let random_string = rand_string(50); + self.0 + .insert(constants::sessions::OIDC_STATE_KEY, random_string.clone())?; + self.0.insert(constants::sessions::OIDC_REMOTE_IP, ip)?; + Ok(random_string) + } + + /// Validate OpenID state + pub fn validate_state(&self, state: &str, ip: IpAddr) -> anyhow::Result<()> { + let session_state: String = self + .0 + .get(constants::sessions::OIDC_STATE_KEY)? + .ok_or(MatrixGWSessionError::OIDCMissingState)?; + + let session_ip: IpAddr = self + .0 + .get(constants::sessions::OIDC_REMOTE_IP)? + .ok_or(MatrixGWSessionError::OIDCMissingIP)?; + + if session_state != state { + return Err(anyhow::anyhow!(MatrixGWSessionError::OIDCInvalidState)); + } + + if session_ip != ip { + return Err(anyhow::anyhow!(MatrixGWSessionError::OIDCInvalidIP)); + } + + Ok(()) + } + + /// Set current user + pub fn set_user(&self, user: &User) -> anyhow::Result<()> { + self.0.insert(constants::sessions::USER_ID, &user.email)?; + Ok(()) + } + + /// Get current user + pub fn current_user(&self) -> anyhow::Result> { + Ok(self.0.get(constants::sessions::USER_ID)?) + } + + /// Remove defined user + pub fn unset_current_user(&self) -> anyhow::Result<()> { + self.0.remove(constants::sessions::USER_ID); + Ok(()) + } +} + +impl FromRequest for MatrixGWSession { + type Error = Error; + type Future = Ready>; + + #[inline] + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ready( + Session::from_request(req, &mut Payload::None) + .into_inner() + .map(MatrixGWSession), + ) + } +} diff --git a/matrixgw_backend/src/lib.rs b/matrixgw_backend/src/lib.rs index 2483671..82c2228 100644 --- a/matrixgw_backend/src/lib.rs +++ b/matrixgw_backend/src/lib.rs @@ -1,2 +1,6 @@ pub mod app_config; +pub mod constants; pub mod controllers; +pub mod extractors; +pub mod users; +pub mod utils; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 3d6132f..3100299 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -5,7 +5,8 @@ use actix_session::storage::RedisSessionStore; use actix_web::cookie::Key; use actix_web::{App, HttpServer, web}; use matrixgw_backend::app_config::AppConfig; -use matrixgw_backend::controllers::server_controller; +use matrixgw_backend::controllers::{auth_controller, server_controller}; +use matrixgw_backend::users::User; #[tokio::main] async fn main() -> std::io::Result<()> { @@ -17,6 +18,13 @@ async fn main() -> std::io::Result<()> { .await .expect("Failed to connect to Redis!"); + // Auto create default account, if requested + if let Some(mail) = &AppConfig::get().unsecure_auto_login_email() { + User::create_or_update_user(mail, "Anonymous") + .await + .expect("Failed to create auto-login account!"); + } + log::info!( "Starting to listen on {} for {}", AppConfig::get().listen_address, @@ -40,6 +48,20 @@ async fn main() -> std::io::Result<()> { "/api/server/config", web::get().to(server_controller::config), ) + // Auth controller + .route( + "/api/auth/start_oidc", + web::get().to(auth_controller::start_oidc), + ) + .route( + "/api/auth/finish_oidc", + web::post().to(auth_controller::finish_oidc), + ) + .route("/api/auth/info", web::get().to(auth_controller::auth_info)) + .route( + "/api/auth/sign_out", + web::get().to(auth_controller::sign_out), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs new file mode 100644 index 0000000..2cb1e0a --- /dev/null +++ b/matrixgw_backend/src/users.rs @@ -0,0 +1,168 @@ +use crate::app_config::AppConfig; +use crate::utils::time_utils::time_secs; +use jwt_simple::reexports::serde_json; +use std::cmp::min; +use std::str::FromStr; + +/// Matrix Gateway user errors +#[derive(thiserror::Error, Debug)] +enum MatrixGWUserError { + #[error("Failed to load user metadata: {0}")] + LoadUserMetadata(std::io::Error), + #[error("Failed to decode user metadata: {0}")] + DecodeUserMetadata(serde_json::Error), + #[error("Failed to save user metadata: {0}")] + SaveUserMetadata(std::io::Error), + #[error("Failed to delete API token: {0}")] + DeleteToken(std::io::Error), + #[error("Failed to load API token: {0}")] + LoadApiToken(std::io::Error), + #[error("Failed to decode API token: {0}")] + DecodeApiToken(serde_json::Error), + #[error("API Token does not exists!")] + ApiTokenDoesNotExists, + #[error("Failed to save API token: {0}")] + SaveAPIToken(std::io::Error), +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +pub struct UserEmail(pub String); + +impl UserEmail { + pub fn is_valid(&self) -> bool { + mailchecker::is_valid(&self.0) + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] +pub struct APITokenID(pub uuid::Uuid); + +impl Default for APITokenID { + fn default() -> Self { + Self(uuid::Uuid::new_v4()) + } +} + +impl FromStr for APITokenID { + type Err = uuid::Error; + + fn from_str(s: &str) -> Result { + Ok(Self(uuid::Uuid::from_str(s)?)) + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct User { + pub email: UserEmail, + pub name: String, + pub time_create: u64, + pub last_login: u64, +} + +impl User { + /// Get a user by its mail + pub async fn get_by_mail(mail: &UserEmail) -> anyhow::Result { + let path = AppConfig::get().user_metadata_file_path(mail); + let data = std::fs::read_to_string(path).map_err(MatrixGWUserError::LoadUserMetadata)?; + Ok(serde_json::from_str(&data).map_err(MatrixGWUserError::DecodeUserMetadata)?) + } + + /// Update user metadata on disk + pub async fn write(&self) -> anyhow::Result<()> { + let path = AppConfig::get().user_metadata_file_path(&self.email); + std::fs::write(&path, serde_json::to_string(&self)?) + .map_err(MatrixGWUserError::SaveUserMetadata)?; + Ok(()) + } + + /// Create or update user information + pub async fn create_or_update_user(mail: &UserEmail, name: &str) -> anyhow::Result { + let storage_dir = AppConfig::get().user_directory(mail); + let mut user = if !storage_dir.exists() { + std::fs::create_dir_all(storage_dir)?; + + User { + email: mail.clone(), + name: name.to_string(), + time_create: time_secs(), + last_login: time_secs(), + } + } else { + Self::get_by_mail(mail).await? + }; + + // Update some user information + user.name = name.to_string(); + user.last_login = time_secs(); + user.write().await?; + + Ok(user) + } +} + +/// Single API client information +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct APIToken { + /// Token unique ID + pub id: APITokenID, + + /// Client description + pub description: String, + + /// Restricted API network for token + pub network: Option, + + /// Client secret + pub secret: String, + + /// Client creation time + pub created: u64, + + /// Client last usage time + pub last_used: u64, + + /// Read only access + pub read_only: bool, + + /// Token max inactivity + pub max_inactivity: u64, +} + +impl APIToken { + /// Get a token information + pub async fn load(email: &UserEmail, id: &APITokenID) -> anyhow::Result { + let token_file = AppConfig::get().user_api_token_metadata_file(email, id); + match token_file.exists() { + true => Ok(serde_json::from_str::( + &std::fs::read_to_string(&token_file).map_err(MatrixGWUserError::LoadApiToken)?, + ) + .map_err(MatrixGWUserError::DecodeApiToken)?), + false => Err(MatrixGWUserError::ApiTokenDoesNotExists.into()), + } + } + + /// Write this token information + pub async fn write(&self, mail: &UserEmail) -> anyhow::Result<()> { + let path = AppConfig::get().user_api_token_metadata_file(mail, &self.id); + std::fs::write(&path, serde_json::to_string(&self)?) + .map_err(MatrixGWUserError::SaveAPIToken)?; + Ok(()) + } + + /// Delete this token + pub async fn delete(self, email: &UserEmail) -> anyhow::Result<()> { + let token_file = AppConfig::get().user_api_token_metadata_file(email, &self.id); + std::fs::remove_file(&token_file).map_err(MatrixGWUserError::DeleteToken)?; + Ok(()) + } + + pub fn shall_update_time_used(&self) -> bool { + let refresh_interval = min(600, self.max_inactivity / 10); + + (self.last_used) < time_secs() - refresh_interval + } + + pub fn is_expired(&self) -> bool { + (self.last_used + self.max_inactivity) < time_secs() + } +} diff --git a/matrixgw_backend/src/utils/crypt_utils.rs b/matrixgw_backend/src/utils/crypt_utils.rs new file mode 100644 index 0000000..9a22fa9 --- /dev/null +++ b/matrixgw_backend/src/utils/crypt_utils.rs @@ -0,0 +1,6 @@ +use sha2::{Digest, Sha256}; + +/// Compute SHA256sum of a given string +pub fn sha256str(input: &str) -> String { + hex::encode(Sha256::digest(input.as_bytes())) +} diff --git a/matrixgw_backend/src/utils/mod.rs b/matrixgw_backend/src/utils/mod.rs new file mode 100644 index 0000000..ff5baec --- /dev/null +++ b/matrixgw_backend/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod crypt_utils; +pub mod rand_utils; +pub mod time_utils; diff --git a/matrixgw_backend/src/utils/rand_utils.rs b/matrixgw_backend/src/utils/rand_utils.rs new file mode 100644 index 0000000..aa40750 --- /dev/null +++ b/matrixgw_backend/src/utils/rand_utils.rs @@ -0,0 +1,6 @@ +use rand::distr::{Alphanumeric, SampleString}; + +/// Generate a random string of a given length +pub fn rand_string(len: usize) -> String { + Alphanumeric.sample_string(&mut rand::rng(), len) +} diff --git a/matrixgw_backend/src/utils/time_utils.rs b/matrixgw_backend/src/utils/time_utils.rs new file mode 100644 index 0000000..ac8c636 --- /dev/null +++ b/matrixgw_backend/src/utils/time_utils.rs @@ -0,0 +1,9 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Get the current time since epoch +pub fn time_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() +} From 1cdd3d9e6061cab0ca2b71dfb8ed8d04405b4f9a Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 3 Nov 2025 22:28:38 +0100 Subject: [PATCH 005/124] Add backend README --- matrixgw_backend/README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 matrixgw_backend/README.md diff --git a/matrixgw_backend/README.md b/matrixgw_backend/README.md new file mode 100644 index 0000000..9c11efa --- /dev/null +++ b/matrixgw_backend/README.md @@ -0,0 +1,2 @@ +# Matrix Gateway backend +Backend component, written in Rust using Actix. From d05747e60e939725949164123e0923d17e07fd79 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 4 Nov 2025 18:58:41 +0100 Subject: [PATCH 006/124] Add base web app --- matrixgw_backend/Cargo.lock | 16 + matrixgw_backend/Cargo.toml | 1 + matrixgw_backend/examples/api_curl.rs | 2 +- matrixgw_backend/src/main.rs | 25 +- matrixgw_frontend/.env | 1 + matrixgw_frontend/.env.production | 1 + matrixgw_frontend/package-lock.json | 763 +++++++++++++++++- matrixgw_frontend/package.json | 5 + matrixgw_frontend/src/App.css | 42 - matrixgw_frontend/src/App.tsx | 36 +- matrixgw_frontend/src/api/ApiClient.ts | 192 +++++ matrixgw_frontend/src/api/AuthApi.ts | 87 ++ matrixgw_frontend/src/api/ServerApi.ts | 48 ++ matrixgw_frontend/src/assets/react.svg | 1 - .../contexts_provider/AlertDialogProvider.tsx | 68 ++ .../ConfirmDialogProvider.tsx | 98 +++ .../LoadingMessageProvider.tsx | 62 ++ .../contexts_provider/SnackbarProvider.tsx | 41 + matrixgw_frontend/src/index.css | 67 +- matrixgw_frontend/src/main.tsx | 42 +- matrixgw_frontend/src/widgets/AsyncWidget.tsx | 94 +++ 21 files changed, 1511 insertions(+), 181 deletions(-) create mode 100644 matrixgw_frontend/.env create mode 100644 matrixgw_frontend/.env.production delete mode 100644 matrixgw_frontend/src/App.css create mode 100644 matrixgw_frontend/src/api/ApiClient.ts create mode 100644 matrixgw_frontend/src/api/AuthApi.ts create mode 100644 matrixgw_frontend/src/api/ServerApi.ts delete mode 100644 matrixgw_frontend/src/assets/react.svg create mode 100644 matrixgw_frontend/src/hooks/contexts_provider/AlertDialogProvider.tsx create mode 100644 matrixgw_frontend/src/hooks/contexts_provider/ConfirmDialogProvider.tsx create mode 100644 matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx create mode 100644 matrixgw_frontend/src/hooks/contexts_provider/SnackbarProvider.tsx create mode 100644 matrixgw_frontend/src/widgets/AsyncWidget.tsx diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index ad5c8da..f857cd7 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -19,6 +19,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "actix-cors" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" +dependencies = [ + "actix-utils", + "actix-web", + "derive_more", + "futures-util", + "log", + "once_cell", + "smallvec", +] + [[package]] name = "actix-http" version = "3.11.2" @@ -1605,6 +1620,7 @@ dependencies = [ name = "matrixgw_backend" version = "0.1.0" dependencies = [ + "actix-cors", "actix-remote-ip", "actix-session", "actix-web", diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index dfb0d9e..ffe9d1e 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -14,6 +14,7 @@ tokio = { version = "1.48.0", features = ["full"] } actix-web = "4.11.0" actix-session = { version = "0.11.0", features = ["redis-session"] } actix-remote-ip = "0.1.0" +actix-cors = "0.7.1" light-openid = "1.0.4" bytes = "1.10.1" sha2 = "0.10.9" diff --git a/matrixgw_backend/examples/api_curl.rs b/matrixgw_backend/examples/api_curl.rs index eb57533..3b8b7ff 100644 --- a/matrixgw_backend/examples/api_curl.rs +++ b/matrixgw_backend/examples/api_curl.rs @@ -3,10 +3,10 @@ use jwt_simple::algorithms::HS256Key; use jwt_simple::prelude::{Clock, Duration, JWTClaims, MACLike}; use matrixgw_backend::constants; use matrixgw_backend::extractors::auth_extractor::TokenClaims; +use matrixgw_backend::utils::rand_utils::rand_string; use std::ops::Add; use std::os::unix::prelude::CommandExt; use std::process::Command; -use matrixgw_backend::utils::rand_utils::rand_string; /// cURL wrapper to query MatrixGW #[derive(Parser, Debug)] diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 3100299..a3fd905 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -1,10 +1,13 @@ +use actix_cors::Cors; use actix_remote_ip::RemoteIPConfig; use actix_session::SessionMiddleware; use actix_session::config::SessionLifecycle; use actix_session::storage::RedisSessionStore; use actix_web::cookie::Key; +use actix_web::middleware::Logger; use actix_web::{App, HttpServer, web}; use matrixgw_backend::app_config::AppConfig; +use matrixgw_backend::constants; use matrixgw_backend::controllers::{auth_controller, server_controller}; use matrixgw_backend::users::User; @@ -32,13 +35,23 @@ async fn main() -> std::io::Result<()> { ); HttpServer::new(move || { + let session_mw = SessionMiddleware::builder(redis_store.clone(), secret_key.clone()) + .cookie_name("matrixgw-session".to_string()) + .session_lifecycle(SessionLifecycle::BrowserSession(Default::default())) + .build(); + + let cors = Cors::default() + .allowed_origin(&AppConfig::get().website_origin) + .allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) + .allowed_header(constants::API_AUTH_HEADER) + .allow_any_header() + .supports_credentials() + .max_age(3600); + App::new() - .wrap( - SessionMiddleware::builder(redis_store.clone(), secret_key.clone()) - .cookie_name("matrixgw-session".to_string()) - .session_lifecycle(SessionLifecycle::BrowserSession(Default::default())) - .build(), - ) + .wrap(Logger::default()) + .wrap(session_mw) + .wrap(cors) .app_data(web::Data::new(RemoteIPConfig { proxy: AppConfig::get().proxy_ip.clone(), })) diff --git a/matrixgw_frontend/.env b/matrixgw_frontend/.env new file mode 100644 index 0000000..38c6b7b --- /dev/null +++ b/matrixgw_frontend/.env @@ -0,0 +1 @@ +VITE_APP_BACKEND=http://localhost:8000/api diff --git a/matrixgw_frontend/.env.production b/matrixgw_frontend/.env.production new file mode 100644 index 0000000..89f62f8 --- /dev/null +++ b/matrixgw_frontend/.env.production @@ -0,0 +1 @@ +VITE_APP_BACKEND=/api diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index 612a943..5f2de0a 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -8,6 +8,11 @@ "name": "matrixgw_frontend", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource/roboto": "^5.2.8", + "@mui/icons-material": "^7.3.5", + "@mui/material": "^7.3.5", "react": "^19.1.1", "react-dom": "^19.1.1" }, @@ -30,7 +35,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -86,7 +90,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.5", @@ -120,7 +123,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -130,7 +132,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -172,7 +173,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -182,7 +182,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -216,7 +215,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.5" @@ -260,11 +258,19 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -279,7 +285,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -298,7 +303,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -342,6 +346,158 @@ "tslib": "^2.4.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -525,6 +681,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fontsource/roboto": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.8.tgz", + "integrity": "sha512-oh9g4Cg3loVMz9MWeKWfDI+ooxxG1aRVetkiKIb2ESS2rrryGecQ/y4pAj4z5A5ebyw450dYRi/c4k/I3UBhHA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -581,7 +746,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -603,7 +767,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -613,20 +776,251 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz", + "integrity": "sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.5.tgz", + "integrity": "sha512-LciL1GLMZ+VlzyHAALSVAR22t8IST4LCXmljcUSx2NOutgO2XnxdIp8ilFbeNf9wpo0iUFbAuoQcB7h+HHIf3A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^7.3.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", + "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/core-downloads-tracker": "^7.3.5", + "@mui/system": "^7.3.5", + "@mui/types": "^7.4.8", + "@mui/utils": "^7.3.5", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.5.tgz", + "integrity": "sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.5.tgz", + "integrity": "sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz", + "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/private-theming": "^7.3.5", + "@mui/styled-engine": "^7.3.5", + "@mui/types": "^7.4.8", + "@mui/utils": "^7.3.5", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.8", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.8.tgz", + "integrity": "sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.5.tgz", + "integrity": "sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.8", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", @@ -698,6 +1092,16 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-beta.41", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.41.tgz", @@ -1023,11 +1427,22 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1043,6 +1458,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", @@ -1408,6 +1832,21 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1487,7 +1926,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1531,6 +1969,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1565,6 +2012,31 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1584,14 +2056,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1622,6 +2092,16 @@ "node": ">=8" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.244", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", @@ -1629,6 +2109,15 @@ "dev": true, "license": "ISC" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1643,7 +2132,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1916,6 +2404,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1969,6 +2463,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2022,6 +2525,33 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2036,7 +2566,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2059,6 +2588,27 @@ "node": ">=0.8.19" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2103,7 +2653,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2123,7 +2672,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2139,6 +2687,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2451,6 +3005,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2474,6 +3034,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2525,7 +3097,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2561,6 +3132,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2615,7 +3195,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -2624,6 +3203,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2644,11 +3241,25 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -2703,6 +3314,23 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2755,6 +3383,12 @@ "react": "^19.2.0" } }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT" + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -2765,11 +3399,46 @@ "node": ">=0.10.0" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -2890,6 +3559,15 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2913,6 +3591,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2926,6 +3610,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3248,6 +3944,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index 33d4639..2025306 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -10,6 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource/roboto": "^5.2.8", + "@mui/icons-material": "^7.3.5", + "@mui/material": "^7.3.5", "react": "^19.1.1", "react-dom": "^19.1.1" }, diff --git a/matrixgw_frontend/src/App.css b/matrixgw_frontend/src/App.css deleted file mode 100644 index b9d355d..0000000 --- a/matrixgw_frontend/src/App.css +++ /dev/null @@ -1,42 +0,0 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index 3d7ded3..c664eaa 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -1,35 +1,3 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' - -function App() { - const [count, setCount] = useState(0) - - return ( - <> - -

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) +export function App(): React.ReactElement { + return <>hello; } - -export default App diff --git a/matrixgw_frontend/src/api/ApiClient.ts b/matrixgw_frontend/src/api/ApiClient.ts new file mode 100644 index 0000000..fdb7da3 --- /dev/null +++ b/matrixgw_frontend/src/api/ApiClient.ts @@ -0,0 +1,192 @@ +import { AuthApi } from "./AuthApi"; + +interface RequestParams { + uri: string; + method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT"; + allowFail?: boolean; + jsonData?: any; + formData?: FormData; + upProgress?: (progress: number) => void; + downProgress?: (e: { progress: number; total: number }) => void; +} + +interface APIResponse { + data: any; + status: number; +} + +export class ApiError extends Error { + public code: number; + public data: number; + constructor(message: string, code: number, data: any) { + super(`HTTP status: ${code}\nMessage: ${message}\nData=${data}`); + this.code = code; + this.data = data; + } +} + +export class APIClient { + /** + * Get backend URL + */ + static backendURL(): string { + const URL = import.meta.env.VITE_APP_BACKEND ?? ""; + if (URL.length === 0) throw new Error("Backend URL undefined!"); + return URL; + } + + /** + * Get the full URL at which the backend can be contacted + */ + static ActualBackendURL(): string { + const backendURL = this.backendURL(); + if (backendURL.startsWith("/")) return `${location.origin}${backendURL}`; + else return backendURL; + } + + /** + * Check out whether the backend is accessed through + * HTTPS or not + */ + static IsBackendSecure(): boolean { + return this.ActualBackendURL().startsWith("https"); + } + + /** + * Perform a request on the backend + */ + static async exec(args: RequestParams): Promise { + let body: string | undefined | FormData = undefined; + const headers: any = {}; + + // JSON request + if (args.jsonData) { + headers["Content-Type"] = "application/json"; + body = JSON.stringify(args.jsonData); + } + + // Form data request + else if (args.formData) { + body = args.formData; + } + + const url = this.backendURL() + args.uri; + + let data; + let status: number; + + // Make the request with XMLHttpRequest + if (args.upProgress) { + const res: XMLHttpRequest = await new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.upload.addEventListener("progress", (e) => { + args.upProgress!(e.loaded / e.total); + }); + xhr.addEventListener("load", () => { + resolve(xhr); + }); + xhr.addEventListener("error", () => { + reject(new Error("File upload failed")); + }); + xhr.addEventListener("abort", () => { + reject(new Error("File upload aborted")); + }); + xhr.addEventListener("timeout", () => { + reject(new Error("File upload timeout")); + }); + xhr.open(args.method, url, true); + xhr.withCredentials = true; + for (const key in headers) { + if (Object.prototype.hasOwnProperty.call(headers, key)) + xhr.setRequestHeader(key, headers[key]); + } + xhr.send(body); + }); + + status = res.status; + if (res.responseType === "json") data = JSON.parse(res.responseText); + else data = res.response; + } + + // Make the request with fetch + else { + const res = await fetch(url, { + method: args.method, + body: body, + headers: headers, + credentials: "include", + }); + + // Process response + // JSON response + if (res.headers.get("content-type") === "application/json") + data = await res.json(); + // Text / XML response + else if ( + ["application/xml", "text/plain"].includes( + res.headers.get("content-type") ?? "" + ) + ) + data = await res.text(); + // Binary file, tracking download progress + else if (res.body !== null && args.downProgress) { + // Track download progress + const contentEncoding = res.headers.get("content-encoding"); + const contentLength = contentEncoding + ? null + : res.headers.get("content-length"); + + const total = parseInt(contentLength ?? "0", 10); + let loaded = 0; + + const resInt = new Response( + new ReadableStream({ + start(controller) { + const reader = res.body!.getReader(); + + const read = async () => { + try { + const ret = await reader.read(); + if (ret.done) { + controller.close(); + return; + } + loaded += ret.value.byteLength; + args.downProgress!({ progress: loaded, total }); + controller.enqueue(ret.value); + read(); + } catch (e) { + console.error(e); + controller.error(e); + } + }; + + read(); + }, + }) + ); + + data = await resInt.blob(); + } + + // Do not track progress (binary file) + else data = await res.blob(); + + status = res.status; + } + + // Handle expired tokens + if (status === 412) { + AuthApi.UnsetAuthenticated(); + window.location.href = "/"; + } + + if (!args.allowFail && (status < 200 || status > 299)) + throw new ApiError("Request failed!", status, data); + + return { + data: data, + status: status, + }; + } +} diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts new file mode 100644 index 0000000..eb4b105 --- /dev/null +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -0,0 +1,87 @@ +import { APIClient } from "./ApiClient"; + +export interface AuthInfo { + id: number; + time_create: number; + time_update: number; + name: string; + email: string; +} + +const TokenStateKey = "auth-state"; + +export class AuthApi { + /** + * Check out whether user is signed in or not + */ + static get SignedIn(): boolean { + return localStorage.getItem(TokenStateKey) !== null; + } + + /** + * Mark user as authenticated + */ + static SetAuthenticated() { + localStorage.setItem(TokenStateKey, ""); + } + + /** + * Un-mark user as authenticated + */ + static UnsetAuthenticated() { + localStorage.removeItem(TokenStateKey); + } + + /** + * Start OpenID login + */ + static async StartOpenIDLogin(): Promise<{ url: string }> { + return ( + await APIClient.exec({ + uri: "/auth/start_oidc", + method: "GET", + }) + ).data; + } + + /** + * Finish OpenID login + */ + static async FinishOpenIDLogin(code: string, state: string): Promise { + await APIClient.exec({ + uri: "/auth/finish_oidc", + method: "POST", + jsonData: { code: code, state: state }, + }); + + this.SetAuthenticated(); + } + + /** + * Get auth information + */ + static async GetAuthInfo(): Promise { + return ( + await APIClient.exec({ + uri: "/auth/info", + method: "GET", + }) + ).data; + } + + /** + * Sign out + */ + static async SignOut(): Promise { + try { + await APIClient.exec({ + uri: "/auth/sign_out", + method: "GET", + }); + } catch (e) { + console.error("Failed to sign out user on API!", e); + } + + this.UnsetAuthenticated(); + } +} diff --git a/matrixgw_frontend/src/api/ServerApi.ts b/matrixgw_frontend/src/api/ServerApi.ts new file mode 100644 index 0000000..eb7d24d --- /dev/null +++ b/matrixgw_frontend/src/api/ServerApi.ts @@ -0,0 +1,48 @@ +import { APIClient } from "./ApiClient"; + +export interface ServerConfig { + auth_disabled: boolean; + oidc_provider_name: string; + constraints: ServerConstraints; +} + +export interface AccountType { + label: string; + code: string; + icon: string; +} + +export interface ServerConstraints { + token_name: LenConstraint; + token_ip_net: LenConstraint; + token_max_inactivity: LenConstraint; +} + +export interface LenConstraint { + min: number; + max: number; +} + +let config: ServerConfig | null = null; + +export class ServerApi { + /** + * Get server configuration + */ + static async LoadConfig(): Promise { + config = ( + await APIClient.exec({ + uri: "/server/config", + method: "GET", + }) + ).data; + } + + /** + * Get cached configuration + */ + static get Config(): ServerConfig { + if (config === null) throw new Error("Missing configuration!"); + return config; + } +} diff --git a/matrixgw_frontend/src/assets/react.svg b/matrixgw_frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/matrixgw_frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/matrixgw_frontend/src/hooks/contexts_provider/AlertDialogProvider.tsx b/matrixgw_frontend/src/hooks/contexts_provider/AlertDialogProvider.tsx new file mode 100644 index 0000000..cdda964 --- /dev/null +++ b/matrixgw_frontend/src/hooks/contexts_provider/AlertDialogProvider.tsx @@ -0,0 +1,68 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from "@mui/material"; +import React, { type PropsWithChildren } from "react"; + +type AlertContext = (message: string, title?: string) => Promise; + +const AlertContextK = React.createContext(null); + +export function AlertDialogProvider(p: PropsWithChildren): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [title, setTitle] = React.useState(undefined); + const [message, setMessage] = React.useState(""); + + const cb = React.useRef void)>(null); + + const handleClose = () => { + setOpen(false); + + if (cb.current !== null) cb.current(); + cb.current = null; + }; + + const hook: AlertContext = (message, title) => { + setTitle(title); + setMessage(message); + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + return ( + <> + {p.children} + + + {title && {title}} + + + {message} + + + + + + + + ); +} + +export function useAlert(): AlertContext { + return React.use(AlertContextK)!; +} diff --git a/matrixgw_frontend/src/hooks/contexts_provider/ConfirmDialogProvider.tsx b/matrixgw_frontend/src/hooks/contexts_provider/ConfirmDialogProvider.tsx new file mode 100644 index 0000000..588dc06 --- /dev/null +++ b/matrixgw_frontend/src/hooks/contexts_provider/ConfirmDialogProvider.tsx @@ -0,0 +1,98 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from "@mui/material"; +import React, { type PropsWithChildren } from "react"; + +type ConfirmContext = ( + message: string | React.ReactElement, + title?: string, + confirmButton?: string +) => Promise; + +const ConfirmContextK = React.createContext(null); + +export function ConfirmDialogProvider( + p: PropsWithChildren +): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [title, setTitle] = React.useState(undefined); + const [message, setMessage] = React.useState(""); + const [confirmButton, setConfirmButton] = React.useState( + undefined + ); + + const cb = React.useRef void)>(null); + + const handleClose = (confirm: boolean) => { + setOpen(false); + + if (cb.current !== null) cb.current(confirm); + cb.current = null; + }; + + const hook: ConfirmContext = (message, title, confirmButton) => { + setTitle(title); + setMessage(message); + setConfirmButton(confirmButton); + setOpen(true); + + return new Promise((res) => { + cb.current = res; + }); + }; + + const keyUp = (e: React.KeyboardEvent) => { + if (e.code === "Enter") handleClose(true); + }; + + return ( + <> + {p.children} + + { + handleClose(false); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + onKeyUp={keyUp} + > + {title && {title}} + + + {message} + + + + + + + + + ); +} + +export function useConfirm(): ConfirmContext { + return React.use(ConfirmContextK)!; +} diff --git a/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx b/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx new file mode 100644 index 0000000..083a985 --- /dev/null +++ b/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx @@ -0,0 +1,62 @@ +import { + CircularProgress, + Dialog, + DialogContent, + DialogContentText, +} from "@mui/material"; +import React, { type PropsWithChildren } from "react"; + +interface LoadingMessageContext { + show: (message: string) => void; + hide: () => void; +} + +const LoadingMessageContextK = + React.createContext(null); + +export function LoadingMessageProvider( + p: PropsWithChildren +): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [message, setMessage] = React.useState(""); + + const hook: LoadingMessageContext = { + show(message) { + setMessage(message); + setOpen(true); + }, + hide() { + setMessage(""); + setOpen(false); + }, + }; + + return ( + <> + {p.children} + + + + +
+ + + {message} +
+
+
+
+ + ); +} + +export function useLoadingMessage(): LoadingMessageContext { + return React.use(LoadingMessageContextK)!; +} diff --git a/matrixgw_frontend/src/hooks/contexts_provider/SnackbarProvider.tsx b/matrixgw_frontend/src/hooks/contexts_provider/SnackbarProvider.tsx new file mode 100644 index 0000000..8f9b861 --- /dev/null +++ b/matrixgw_frontend/src/hooks/contexts_provider/SnackbarProvider.tsx @@ -0,0 +1,41 @@ +import { Snackbar } from "@mui/material"; + +import React, { type PropsWithChildren } from "react"; + +type SnackbarContext = (message: string, duration?: number) => void; + +const SnackbarContextK = React.createContext(null); + +export function SnackbarProvider(p: PropsWithChildren): React.ReactElement { + const [open, setOpen] = React.useState(false); + + const [message, setMessage] = React.useState(""); + const [duration, setDuration] = React.useState(0); + + const handleClose = () => { + setOpen(false); + }; + + const hook: SnackbarContext = (message, duration) => { + setMessage(message); + setDuration(duration ?? 6000); + setOpen(true); + }; + + return ( + <> + {p.children} + + + + ); +} + +export function useSnackbar(): SnackbarContext { + return React.use(SnackbarContextK)!; +} diff --git a/matrixgw_frontend/src/index.css b/matrixgw_frontend/src/index.css index 08a3ac9..b303f80 100644 --- a/matrixgw_frontend/src/index.css +++ b/matrixgw_frontend/src/index.css @@ -1,68 +1,9 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - body { margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; } -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } +html, +body, +#root { + height: 100%; } diff --git a/matrixgw_frontend/src/main.tsx b/matrixgw_frontend/src/main.tsx index bef5202..cc8c1bb 100644 --- a/matrixgw_frontend/src/main.tsx +++ b/matrixgw_frontend/src/main.tsx @@ -1,10 +1,36 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import "@fontsource/roboto/300.css"; +import "@fontsource/roboto/400.css"; +import "@fontsource/roboto/500.css"; +import "@fontsource/roboto/700.css"; -createRoot(document.getElementById('root')!).render( +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import { App } from "./App"; +import { AlertDialogProvider } from "./hooks/contexts_provider/AlertDialogProvider"; +import { ConfirmDialogProvider } from "./hooks/contexts_provider/ConfirmDialogProvider"; +import { SnackbarProvider } from "./hooks/contexts_provider/SnackbarProvider"; +import { LoadingMessageProvider } from "./hooks/contexts_provider/LoadingMessageProvider"; +import { AsyncWidget } from "./widgets/AsyncWidget"; +import { ServerApi } from "./api/ServerApi"; + +createRoot(document.getElementById("root")!).render( - - , -) + + + + + { + await ServerApi.LoadConfig(); + }} + errMsg="Failed to load static server configuration!" + build={() => } + /> + + + + + +); diff --git a/matrixgw_frontend/src/widgets/AsyncWidget.tsx b/matrixgw_frontend/src/widgets/AsyncWidget.tsx new file mode 100644 index 0000000..d642a7f --- /dev/null +++ b/matrixgw_frontend/src/widgets/AsyncWidget.tsx @@ -0,0 +1,94 @@ +import { Alert, Box, Button, CircularProgress } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; + +const State = { + Loading: 0, + Ready: 1, + Error: 2, +} as const; + +type State = keyof typeof State; + +export function AsyncWidget(p: { + loadKey: any; + load: () => Promise; + errMsg: string; + build: () => React.ReactElement; + ready?: boolean; + errAdditionalElement?: () => React.ReactElement; +}): React.ReactElement { + const [state, setState] = useState(State.Loading); + + const counter = useRef(null); + + const load = async () => { + try { + setState(State.Loading); + await p.load(); + setState(State.Ready); + } catch (e) { + console.error(e); + setState(State.Error); + } + }; + + useEffect(() => { + if (counter.current === p.loadKey) return; + counter.current = p.loadKey; + + load(); + }); + + if (state === State.Error) + return ( + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + }} + > + + {p.errMsg} + + + + + {p.errAdditionalElement?.()} + + ); + + if (state === State.Loading || p.ready === false) + return ( + + theme.palette.mode === "light" + ? theme.palette.grey[100] + : theme.palette.grey[900], + }} + > + + + ); + + return p.build(); +} From 20a42f3c554ac53e72754365efeb4960ad2569f1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 4 Nov 2025 19:20:17 +0100 Subject: [PATCH 007/124] Add custom theme --- matrixgw_frontend/src/App.tsx | 2 +- matrixgw_frontend/src/main.tsx | 37 +- matrixgw_frontend/src/theme/AppTheme.tsx | 46 ++ matrixgw_frontend/src/theme/README.md | 2 + .../src/theme/customizations/dataDisplay.tsx | 233 +++++++++ .../src/theme/customizations/feedback.tsx | 46 ++ .../src/theme/customizations/inputs.tsx | 452 ++++++++++++++++++ .../src/theme/customizations/navigation.tsx | 284 +++++++++++ .../src/theme/customizations/surfaces.ts | 113 +++++ .../src/theme/themePrimitives.ts | 414 ++++++++++++++++ matrixgw_frontend/src/widgets/AsyncWidget.tsx | 8 - 11 files changed, 1612 insertions(+), 25 deletions(-) create mode 100644 matrixgw_frontend/src/theme/AppTheme.tsx create mode 100644 matrixgw_frontend/src/theme/README.md create mode 100644 matrixgw_frontend/src/theme/customizations/dataDisplay.tsx create mode 100644 matrixgw_frontend/src/theme/customizations/feedback.tsx create mode 100644 matrixgw_frontend/src/theme/customizations/inputs.tsx create mode 100644 matrixgw_frontend/src/theme/customizations/navigation.tsx create mode 100644 matrixgw_frontend/src/theme/customizations/surfaces.ts create mode 100644 matrixgw_frontend/src/theme/themePrimitives.ts diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index c664eaa..20351d8 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -1,3 +1,3 @@ export function App(): React.ReactElement { - return <>hello; + return <>hello world; } diff --git a/matrixgw_frontend/src/main.tsx b/matrixgw_frontend/src/main.tsx index cc8c1bb..afd73ad 100644 --- a/matrixgw_frontend/src/main.tsx +++ b/matrixgw_frontend/src/main.tsx @@ -13,24 +13,29 @@ import { SnackbarProvider } from "./hooks/contexts_provider/SnackbarProvider"; import { LoadingMessageProvider } from "./hooks/contexts_provider/LoadingMessageProvider"; import { AsyncWidget } from "./widgets/AsyncWidget"; import { ServerApi } from "./api/ServerApi"; +import { AppTheme } from "./theme/AppTheme"; +import { CssBaseline } from "@mui/material"; createRoot(document.getElementById("root")!).render( - - - - - { - await ServerApi.LoadConfig(); - }} - errMsg="Failed to load static server configuration!" - build={() => } - /> - - - - + + + + + + + { + await ServerApi.LoadConfig(); + }} + errMsg="Failed to load static server configuration!" + build={() => } + /> + + + + + ); diff --git a/matrixgw_frontend/src/theme/AppTheme.tsx b/matrixgw_frontend/src/theme/AppTheme.tsx new file mode 100644 index 0000000..2c432a5 --- /dev/null +++ b/matrixgw_frontend/src/theme/AppTheme.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import { ThemeProvider, createTheme } from "@mui/material/styles"; +import type { ThemeOptions } from "@mui/material/styles"; +import { inputsCustomizations } from "./customizations/inputs"; +import { dataDisplayCustomizations } from "./customizations/dataDisplay"; +import { feedbackCustomizations } from "./customizations/feedback"; +import { navigationCustomizations } from "./customizations/navigation"; +import { surfacesCustomizations } from "./customizations/surfaces"; +import { colorSchemes, typography, shadows, shape } from "./themePrimitives"; + +interface AppThemeProps { + themeComponents?: ThemeOptions["components"]; +} + +export function AppTheme( + props: React.PropsWithChildren +): React.ReactElement { + const { children, themeComponents } = props; + const theme = React.useMemo(() => { + return createTheme({ + // For more details about CSS variables configuration, see https://mui.com/material-ui/customization/css-theme-variables/configuration/ + cssVariables: { + colorSchemeSelector: "data-mui-color-scheme", + cssVarPrefix: "template", + }, + colorSchemes, // Recently added in v6 for building light & dark mode app, see https://mui.com/material-ui/customization/palette/#color-schemes + typography, + shadows, + shape, + components: { + ...inputsCustomizations, + ...dataDisplayCustomizations, + ...feedbackCustomizations, + ...navigationCustomizations, + ...surfacesCustomizations, + ...themeComponents, + }, + }); + }, [themeComponents]); + + return ( + + {children} + + ); +} diff --git a/matrixgw_frontend/src/theme/README.md b/matrixgw_frontend/src/theme/README.md new file mode 100644 index 0000000..e603e6a --- /dev/null +++ b/matrixgw_frontend/src/theme/README.md @@ -0,0 +1,2 @@ +# Application Theme +Taken from https://github.com/mui/material-ui/tree/v7.3.4/docs/data/material/getting-started/templates/shared-theme \ No newline at end of file diff --git a/matrixgw_frontend/src/theme/customizations/dataDisplay.tsx b/matrixgw_frontend/src/theme/customizations/dataDisplay.tsx new file mode 100644 index 0000000..c23166a --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/dataDisplay.tsx @@ -0,0 +1,233 @@ +import { buttonBaseClasses } from "@mui/material/ButtonBase"; +import { chipClasses } from "@mui/material/Chip"; +import { iconButtonClasses } from "@mui/material/IconButton"; +import { alpha, type Components, type Theme } from "@mui/material/styles"; +import { svgIconClasses } from "@mui/material/SvgIcon"; +import { typographyClasses } from "@mui/material/Typography"; +import { gray, green, red } from "../themePrimitives"; + +/* eslint-disable import/prefer-default-export */ +export const dataDisplayCustomizations: Components = { + MuiList: { + styleOverrides: { + root: { + padding: "8px", + display: "flex", + flexDirection: "column", + gap: 0, + }, + }, + }, + MuiListItem: { + styleOverrides: { + root: ({ theme }) => ({ + [`& .${svgIconClasses.root}`]: { + width: "1rem", + height: "1rem", + color: (theme.vars || theme).palette.text.secondary, + }, + [`& .${typographyClasses.root}`]: { + fontWeight: 500, + }, + [`& .${buttonBaseClasses.root}`]: { + display: "flex", + gap: 8, + padding: "2px 8px", + borderRadius: (theme.vars || theme).shape.borderRadius, + opacity: 0.7, + "&.Mui-selected": { + opacity: 1, + backgroundColor: alpha(theme.palette.action.selected, 0.3), + [`& .${svgIconClasses.root}`]: { + color: (theme.vars || theme).palette.text.primary, + }, + "&:focus-visible": { + backgroundColor: alpha(theme.palette.action.selected, 0.3), + }, + "&:hover": { + backgroundColor: alpha(theme.palette.action.selected, 0.5), + }, + }, + "&:focus-visible": { + backgroundColor: "transparent", + }, + }, + }), + }, + }, + MuiListItemText: { + styleOverrides: { + primary: ({ theme }) => ({ + fontSize: theme.typography.body2.fontSize, + fontWeight: 500, + lineHeight: theme.typography.body2.lineHeight, + }), + secondary: ({ theme }) => ({ + fontSize: theme.typography.caption.fontSize, + lineHeight: theme.typography.caption.lineHeight, + }), + }, + }, + MuiListSubheader: { + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: "transparent", + padding: "4px 8px", + fontSize: theme.typography.caption.fontSize, + fontWeight: 500, + lineHeight: theme.typography.caption.lineHeight, + }), + }, + }, + MuiListItemIcon: { + styleOverrides: { + root: { + minWidth: 0, + }, + }, + }, + MuiChip: { + defaultProps: { + size: "small", + }, + styleOverrides: { + root: ({ theme }) => ({ + border: "1px solid", + borderRadius: "999px", + [`& .${chipClasses.label}`]: { + fontWeight: 600, + }, + variants: [ + { + props: { + color: "default", + }, + style: { + borderColor: gray[200], + backgroundColor: gray[100], + [`& .${chipClasses.label}`]: { + color: gray[500], + }, + [`& .${chipClasses.icon}`]: { + color: gray[500], + }, + ...theme.applyStyles("dark", { + borderColor: gray[700], + backgroundColor: gray[800], + [`& .${chipClasses.label}`]: { + color: gray[300], + }, + [`& .${chipClasses.icon}`]: { + color: gray[300], + }, + }), + }, + }, + { + props: { + color: "success", + }, + style: { + borderColor: green[200], + backgroundColor: green[50], + [`& .${chipClasses.label}`]: { + color: green[500], + }, + [`& .${chipClasses.icon}`]: { + color: green[500], + }, + ...theme.applyStyles("dark", { + borderColor: green[800], + backgroundColor: green[900], + [`& .${chipClasses.label}`]: { + color: green[300], + }, + [`& .${chipClasses.icon}`]: { + color: green[300], + }, + }), + }, + }, + { + props: { + color: "error", + }, + style: { + borderColor: red[100], + backgroundColor: red[50], + [`& .${chipClasses.label}`]: { + color: red[500], + }, + [`& .${chipClasses.icon}`]: { + color: red[500], + }, + ...theme.applyStyles("dark", { + borderColor: red[800], + backgroundColor: red[900], + [`& .${chipClasses.label}`]: { + color: red[200], + }, + [`& .${chipClasses.icon}`]: { + color: red[300], + }, + }), + }, + }, + { + props: { size: "small" }, + style: { + maxHeight: 20, + [`& .${chipClasses.label}`]: { + fontSize: theme.typography.caption.fontSize, + }, + [`& .${svgIconClasses.root}`]: { + fontSize: theme.typography.caption.fontSize, + }, + }, + }, + { + props: { size: "medium" }, + style: { + [`& .${chipClasses.label}`]: { + fontSize: theme.typography.caption.fontSize, + }, + }, + }, + ], + }), + }, + }, + MuiTablePagination: { + styleOverrides: { + actions: { + display: "flex", + gap: 8, + marginRight: 6, + [`& .${iconButtonClasses.root}`]: { + minWidth: 0, + width: 36, + height: 36, + }, + }, + }, + }, + MuiIcon: { + defaultProps: { + fontSize: "small", + }, + styleOverrides: { + root: { + variants: [ + { + props: { + fontSize: "small", + }, + style: { + fontSize: "1rem", + }, + }, + ], + }, + }, + }, +}; diff --git a/matrixgw_frontend/src/theme/customizations/feedback.tsx b/matrixgw_frontend/src/theme/customizations/feedback.tsx new file mode 100644 index 0000000..b8dc04b --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/feedback.tsx @@ -0,0 +1,46 @@ +import { type Theme, alpha, type Components } from "@mui/material/styles"; +import { gray, orange } from "../themePrimitives"; + +/* eslint-disable import/prefer-default-export */ +export const feedbackCustomizations: Components = { + MuiAlert: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: 10, + backgroundColor: orange[100], + color: (theme.vars || theme).palette.text.primary, + border: `1px solid ${alpha(orange[300], 0.5)}`, + "& .MuiAlert-icon": { + color: orange[500], + }, + ...theme.applyStyles("dark", { + backgroundColor: `${alpha(orange[900], 0.5)}`, + border: `1px solid ${alpha(orange[800], 0.5)}`, + }), + }), + }, + }, + MuiDialog: { + styleOverrides: { + root: ({ theme }) => ({ + "& .MuiDialog-paper": { + borderRadius: "10px", + border: "1px solid", + borderColor: (theme.vars || theme).palette.divider, + }, + }), + }, + }, + MuiLinearProgress: { + styleOverrides: { + root: ({ theme }) => ({ + height: 8, + borderRadius: 8, + backgroundColor: gray[200], + ...theme.applyStyles("dark", { + backgroundColor: gray[800], + }), + }), + }, + }, +}; diff --git a/matrixgw_frontend/src/theme/customizations/inputs.tsx b/matrixgw_frontend/src/theme/customizations/inputs.tsx new file mode 100644 index 0000000..d30fcf2 --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/inputs.tsx @@ -0,0 +1,452 @@ +import { alpha, type Theme, type Components } from "@mui/material/styles"; +import { outlinedInputClasses } from "@mui/material/OutlinedInput"; +import { svgIconClasses } from "@mui/material/SvgIcon"; +import { toggleButtonGroupClasses } from "@mui/material/ToggleButtonGroup"; +import { toggleButtonClasses } from "@mui/material/ToggleButton"; +import CheckBoxOutlineBlankRoundedIcon from "@mui/icons-material/CheckBoxOutlineBlankRounded"; +import CheckRoundedIcon from "@mui/icons-material/CheckRounded"; +import RemoveRoundedIcon from "@mui/icons-material/RemoveRounded"; +import { gray, brand } from "../themePrimitives"; + +/* eslint-disable import/prefer-default-export */ +export const inputsCustomizations: Components = { + MuiButtonBase: { + defaultProps: { + disableTouchRipple: true, + disableRipple: true, + }, + styleOverrides: { + root: ({ theme }) => ({ + boxSizing: "border-box", + transition: "all 100ms ease-in", + "&:focus-visible": { + outline: `3px solid ${alpha(theme.palette.primary.main, 0.5)}`, + outlineOffset: "2px", + }, + }), + }, + }, + MuiButton: { + styleOverrides: { + root: ({ theme }) => ({ + boxShadow: "none", + borderRadius: (theme.vars || theme).shape.borderRadius, + textTransform: "none", + variants: [ + { + props: { + size: "small", + }, + style: { + height: "2.25rem", + padding: "8px 12px", + }, + }, + { + props: { + size: "medium", + }, + style: { + height: "2.5rem", // 40px + }, + }, + { + props: { + color: "primary", + variant: "contained", + }, + style: { + color: "white", + backgroundColor: gray[900], + backgroundImage: `linear-gradient(to bottom, ${gray[700]}, ${gray[800]})`, + boxShadow: `inset 0 1px 0 ${gray[600]}, inset 0 -1px 0 1px hsl(220, 0%, 0%)`, + border: `1px solid ${gray[700]}`, + "&:hover": { + backgroundImage: "none", + backgroundColor: gray[700], + boxShadow: "none", + }, + "&:active": { + backgroundColor: gray[800], + }, + ...theme.applyStyles("dark", { + color: "black", + backgroundColor: gray[50], + backgroundImage: `linear-gradient(to bottom, ${gray[100]}, ${gray[50]})`, + boxShadow: "inset 0 -1px 0 hsl(220, 30%, 80%)", + border: `1px solid ${gray[50]}`, + "&:hover": { + backgroundImage: "none", + backgroundColor: gray[300], + boxShadow: "none", + }, + "&:active": { + backgroundColor: gray[400], + }, + }), + }, + }, + { + props: { + color: "secondary", + variant: "contained", + }, + style: { + color: "white", + backgroundColor: brand[300], + backgroundImage: `linear-gradient(to bottom, ${alpha( + brand[400], + 0.8 + )}, ${brand[500]})`, + boxShadow: `inset 0 2px 0 ${alpha( + brand[200], + 0.2 + )}, inset 0 -2px 0 ${alpha(brand[700], 0.4)}`, + border: `1px solid ${brand[500]}`, + "&:hover": { + backgroundColor: brand[700], + boxShadow: "none", + }, + "&:active": { + backgroundColor: brand[700], + backgroundImage: "none", + }, + }, + }, + { + props: { + variant: "outlined", + }, + style: { + color: (theme.vars || theme).palette.text.primary, + border: "1px solid", + borderColor: gray[200], + backgroundColor: alpha(gray[50], 0.3), + "&:hover": { + backgroundColor: gray[100], + borderColor: gray[300], + }, + "&:active": { + backgroundColor: gray[200], + }, + ...theme.applyStyles("dark", { + backgroundColor: gray[800], + borderColor: gray[700], + + "&:hover": { + backgroundColor: gray[900], + borderColor: gray[600], + }, + "&:active": { + backgroundColor: gray[900], + }, + }), + }, + }, + { + props: { + color: "secondary", + variant: "outlined", + }, + style: { + color: brand[700], + border: "1px solid", + borderColor: brand[200], + backgroundColor: brand[50], + "&:hover": { + backgroundColor: brand[100], + borderColor: brand[400], + }, + "&:active": { + backgroundColor: alpha(brand[200], 0.7), + }, + ...theme.applyStyles("dark", { + color: brand[50], + border: "1px solid", + borderColor: brand[900], + backgroundColor: alpha(brand[900], 0.3), + "&:hover": { + borderColor: brand[700], + backgroundColor: alpha(brand[900], 0.6), + }, + "&:active": { + backgroundColor: alpha(brand[900], 0.5), + }, + }), + }, + }, + { + props: { + variant: "text", + }, + style: { + color: gray[600], + "&:hover": { + backgroundColor: gray[100], + }, + "&:active": { + backgroundColor: gray[200], + }, + ...theme.applyStyles("dark", { + color: gray[50], + "&:hover": { + backgroundColor: gray[700], + }, + "&:active": { + backgroundColor: alpha(gray[700], 0.7), + }, + }), + }, + }, + { + props: { + color: "secondary", + variant: "text", + }, + style: { + color: brand[700], + "&:hover": { + backgroundColor: alpha(brand[100], 0.5), + }, + "&:active": { + backgroundColor: alpha(brand[200], 0.7), + }, + ...theme.applyStyles("dark", { + color: brand[100], + "&:hover": { + backgroundColor: alpha(brand[900], 0.5), + }, + "&:active": { + backgroundColor: alpha(brand[900], 0.3), + }, + }), + }, + }, + ], + }), + }, + }, + MuiIconButton: { + styleOverrides: { + root: ({ theme }) => ({ + boxShadow: "none", + borderRadius: (theme.vars || theme).shape.borderRadius, + textTransform: "none", + fontWeight: theme.typography.fontWeightMedium, + letterSpacing: 0, + color: (theme.vars || theme).palette.text.primary, + border: "1px solid ", + borderColor: gray[200], + backgroundColor: alpha(gray[50], 0.3), + "&:hover": { + backgroundColor: gray[100], + borderColor: gray[300], + }, + "&:active": { + backgroundColor: gray[200], + }, + ...theme.applyStyles("dark", { + backgroundColor: gray[800], + borderColor: gray[700], + "&:hover": { + backgroundColor: gray[900], + borderColor: gray[600], + }, + "&:active": { + backgroundColor: gray[900], + }, + }), + variants: [ + { + props: { + size: "small", + }, + style: { + width: "2.25rem", + height: "2.25rem", + padding: "0.25rem", + [`& .${svgIconClasses.root}`]: { fontSize: "1rem" }, + }, + }, + { + props: { + size: "medium", + }, + style: { + width: "2.5rem", + height: "2.5rem", + }, + }, + ], + }), + }, + }, + MuiToggleButtonGroup: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: "10px", + boxShadow: `0 4px 16px ${alpha(gray[400], 0.2)}`, + [`& .${toggleButtonGroupClasses.selected}`]: { + color: brand[500], + }, + ...theme.applyStyles("dark", { + [`& .${toggleButtonGroupClasses.selected}`]: { + color: "#fff", + }, + boxShadow: `0 4px 16px ${alpha(brand[700], 0.5)}`, + }), + }), + }, + }, + MuiToggleButton: { + styleOverrides: { + root: ({ theme }) => ({ + padding: "12px 16px", + textTransform: "none", + borderRadius: "10px", + fontWeight: 500, + ...theme.applyStyles("dark", { + color: gray[400], + boxShadow: "0 4px 16px rgba(0, 0, 0, 0.5)", + [`&.${toggleButtonClasses.selected}`]: { + color: brand[300], + }, + }), + }), + }, + }, + MuiCheckbox: { + defaultProps: { + disableRipple: true, + icon: ( + + ), + checkedIcon: , + indeterminateIcon: , + }, + styleOverrides: { + root: ({ theme }) => ({ + margin: 10, + height: 16, + width: 16, + borderRadius: 5, + border: "1px solid ", + borderColor: alpha(gray[300], 0.8), + boxShadow: "0 0 0 1.5px hsla(210, 0%, 0%, 0.04) inset", + backgroundColor: alpha(gray[100], 0.4), + transition: "border-color, background-color, 120ms ease-in", + "&:hover": { + borderColor: brand[300], + }, + "&.Mui-focusVisible": { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: "2px", + borderColor: brand[400], + }, + "&.Mui-checked": { + color: "white", + backgroundColor: brand[500], + borderColor: brand[500], + boxShadow: `none`, + "&:hover": { + backgroundColor: brand[600], + }, + }, + ...theme.applyStyles("dark", { + borderColor: alpha(gray[700], 0.8), + boxShadow: "0 0 0 1.5px hsl(210, 0%, 0%) inset", + backgroundColor: alpha(gray[900], 0.8), + "&:hover": { + borderColor: brand[300], + }, + "&.Mui-focusVisible": { + borderColor: brand[400], + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: "2px", + }, + }), + }), + }, + }, + MuiInputBase: { + styleOverrides: { + root: { + border: "none", + }, + input: { + "&::placeholder": { + opacity: 0.7, + color: gray[500], + }, + }, + }, + }, + MuiOutlinedInput: { + styleOverrides: { + input: { + padding: 0, + }, + root: ({ theme }) => ({ + padding: "8px 12px", + color: (theme.vars || theme).palette.text.primary, + borderRadius: (theme.vars || theme).shape.borderRadius, + border: `1px solid ${(theme.vars || theme).palette.divider}`, + backgroundColor: (theme.vars || theme).palette.background.default, + transition: "border 120ms ease-in", + "&:hover": { + borderColor: gray[400], + }, + [`&.${outlinedInputClasses.focused}`]: { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + borderColor: brand[400], + }, + ...theme.applyStyles("dark", { + "&:hover": { + borderColor: gray[500], + }, + }), + variants: [ + { + props: { + size: "small", + }, + style: { + height: "2.25rem", + }, + }, + { + props: { + size: "medium", + }, + style: { + height: "2.5rem", + }, + }, + ], + }), + notchedOutline: { + border: "none", + }, + }, + }, + MuiInputAdornment: { + styleOverrides: { + root: ({ theme }) => ({ + color: (theme.vars || theme).palette.grey[500], + ...theme.applyStyles("dark", { + color: (theme.vars || theme).palette.grey[400], + }), + }), + }, + }, + MuiFormLabel: { + styleOverrides: { + root: ({ theme }) => ({ + typography: theme.typography.caption, + marginBottom: 8, + }), + }, + }, +}; diff --git a/matrixgw_frontend/src/theme/customizations/navigation.tsx b/matrixgw_frontend/src/theme/customizations/navigation.tsx new file mode 100644 index 0000000..2b1a584 --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/navigation.tsx @@ -0,0 +1,284 @@ +import * as React from "react"; +import { type Theme, alpha, type Components } from "@mui/material/styles"; +import { type SvgIconProps } from "@mui/material/SvgIcon"; +import { buttonBaseClasses } from "@mui/material/ButtonBase"; +import { dividerClasses } from "@mui/material/Divider"; +import { menuItemClasses } from "@mui/material/MenuItem"; +import { selectClasses } from "@mui/material/Select"; +import { tabClasses } from "@mui/material/Tab"; +import UnfoldMoreRoundedIcon from "@mui/icons-material/UnfoldMoreRounded"; +import { gray, brand } from "../themePrimitives"; + +/* eslint-disable import/prefer-default-export */ +export const navigationCustomizations: Components = { + MuiMenuItem: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: (theme.vars || theme).shape.borderRadius, + padding: "6px 8px", + [`&.${menuItemClasses.focusVisible}`]: { + backgroundColor: "transparent", + }, + [`&.${menuItemClasses.selected}`]: { + [`&.${menuItemClasses.focusVisible}`]: { + backgroundColor: alpha(theme.palette.action.selected, 0.3), + }, + }, + }), + }, + }, + MuiMenu: { + styleOverrides: { + list: { + gap: "0px", + [`&.${dividerClasses.root}`]: { + margin: "0 -8px", + }, + }, + paper: ({ theme }) => ({ + marginTop: "4px", + borderRadius: (theme.vars || theme).shape.borderRadius, + border: `1px solid ${(theme.vars || theme).palette.divider}`, + backgroundImage: "none", + background: "hsl(0, 0%, 100%)", + boxShadow: + "hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px", + [`& .${buttonBaseClasses.root}`]: { + "&.Mui-selected": { + backgroundColor: alpha(theme.palette.action.selected, 0.3), + }, + }, + ...theme.applyStyles("dark", { + background: gray[900], + boxShadow: + "hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px", + }), + }), + }, + }, + MuiSelect: { + defaultProps: { + IconComponent: React.forwardRef( + (props, ref) => ( + + ) + ), + }, + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: (theme.vars || theme).shape.borderRadius, + border: "1px solid", + borderColor: gray[200], + backgroundColor: (theme.vars || theme).palette.background.paper, + boxShadow: `inset 0 1px 0 1px hsla(220, 0%, 100%, 0.6), inset 0 -1px 0 1px hsla(220, 35%, 90%, 0.5)`, + "&:hover": { + borderColor: gray[300], + backgroundColor: (theme.vars || theme).palette.background.paper, + boxShadow: "none", + }, + [`&.${selectClasses.focused}`]: { + outlineOffset: 0, + borderColor: gray[400], + }, + "&:before, &:after": { + display: "none", + }, + + ...theme.applyStyles("dark", { + borderRadius: (theme.vars || theme).shape.borderRadius, + borderColor: gray[700], + backgroundColor: (theme.vars || theme).palette.background.paper, + boxShadow: `inset 0 1px 0 1px ${alpha( + gray[700], + 0.15 + )}, inset 0 -1px 0 1px hsla(220, 0%, 0%, 0.7)`, + "&:hover": { + borderColor: alpha(gray[700], 0.7), + backgroundColor: (theme.vars || theme).palette.background.paper, + boxShadow: "none", + }, + [`&.${selectClasses.focused}`]: { + outlineOffset: 0, + borderColor: gray[900], + }, + "&:before, &:after": { + display: "none", + }, + }), + }), + select: ({ theme }) => ({ + display: "flex", + alignItems: "center", + ...theme.applyStyles("dark", { + display: "flex", + alignItems: "center", + "&:focus-visible": { + backgroundColor: gray[900], + }, + }), + }), + }, + }, + MuiLink: { + defaultProps: { + underline: "none", + }, + styleOverrides: { + root: ({ theme }) => ({ + color: (theme.vars || theme).palette.text.primary, + fontWeight: 500, + position: "relative", + textDecoration: "none", + width: "fit-content", + "&::before": { + content: '""', + position: "absolute", + width: "100%", + height: "1px", + bottom: 0, + left: 0, + backgroundColor: (theme.vars || theme).palette.text.secondary, + opacity: 0.3, + transition: "width 0.3s ease, opacity 0.3s ease", + }, + "&:hover::before": { + width: 0, + }, + "&:focus-visible": { + outline: `3px solid ${alpha(brand[500], 0.5)}`, + outlineOffset: "4px", + borderRadius: "2px", + }, + }), + }, + }, + MuiDrawer: { + styleOverrides: { + paper: ({ theme }) => ({ + backgroundColor: (theme.vars || theme).palette.background.default, + }), + }, + }, + MuiPaginationItem: { + styleOverrides: { + root: ({ theme }) => ({ + "&.Mui-selected": { + color: "white", + backgroundColor: (theme.vars || theme).palette.grey[900], + }, + ...theme.applyStyles("dark", { + "&.Mui-selected": { + color: "black", + backgroundColor: (theme.vars || theme).palette.grey[50], + }, + }), + }), + }, + }, + MuiTabs: { + styleOverrides: { + root: { minHeight: "fit-content" }, + indicator: ({ theme }) => ({ + backgroundColor: (theme.vars || theme).palette.grey[800], + ...theme.applyStyles("dark", { + backgroundColor: (theme.vars || theme).palette.grey[200], + }), + }), + }, + }, + MuiTab: { + styleOverrides: { + root: ({ theme }) => ({ + padding: "6px 8px", + marginBottom: "8px", + textTransform: "none", + minWidth: "fit-content", + minHeight: "fit-content", + color: (theme.vars || theme).palette.text.secondary, + borderRadius: (theme.vars || theme).shape.borderRadius, + border: "1px solid", + borderColor: "transparent", + ":hover": { + color: (theme.vars || theme).palette.text.primary, + backgroundColor: gray[100], + borderColor: gray[200], + }, + [`&.${tabClasses.selected}`]: { + color: gray[900], + }, + ...theme.applyStyles("dark", { + ":hover": { + color: (theme.vars || theme).palette.text.primary, + backgroundColor: gray[800], + borderColor: gray[700], + }, + [`&.${tabClasses.selected}`]: { + color: "#fff", + }, + }), + }), + }, + }, + MuiStepConnector: { + styleOverrides: { + line: ({ theme }) => ({ + borderTop: "1px solid", + borderColor: (theme.vars || theme).palette.divider, + flex: 1, + borderRadius: "99px", + }), + }, + }, + MuiStepIcon: { + styleOverrides: { + root: ({ theme }) => ({ + color: "transparent", + border: `1px solid ${gray[400]}`, + width: 12, + height: 12, + borderRadius: "50%", + "& text": { + display: "none", + }, + "&.Mui-active": { + border: "none", + color: (theme.vars || theme).palette.primary.main, + }, + "&.Mui-completed": { + border: "none", + color: (theme.vars || theme).palette.success.main, + }, + ...theme.applyStyles("dark", { + border: `1px solid ${gray[700]}`, + "&.Mui-active": { + border: "none", + color: (theme.vars || theme).palette.primary.light, + }, + "&.Mui-completed": { + border: "none", + color: (theme.vars || theme).palette.success.light, + }, + }), + variants: [ + { + props: { completed: true }, + style: { + width: 12, + height: 12, + }, + }, + ], + }), + }, + }, + MuiStepLabel: { + styleOverrides: { + label: ({ theme }) => ({ + "&.Mui-completed": { + opacity: 0.6, + ...theme.applyStyles("dark", { opacity: 0.5 }), + }, + }), + }, + }, +}; diff --git a/matrixgw_frontend/src/theme/customizations/surfaces.ts b/matrixgw_frontend/src/theme/customizations/surfaces.ts new file mode 100644 index 0000000..38bf7ed --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/surfaces.ts @@ -0,0 +1,113 @@ +import { alpha, type Theme, type Components } from "@mui/material/styles"; +import { gray } from "../themePrimitives"; + +/* eslint-disable import/prefer-default-export */ +export const surfacesCustomizations: Components = { + MuiAccordion: { + defaultProps: { + elevation: 0, + disableGutters: true, + }, + styleOverrides: { + root: ({ theme }) => ({ + padding: 4, + overflow: "clip", + backgroundColor: (theme.vars || theme).palette.background.default, + border: "1px solid", + borderColor: (theme.vars || theme).palette.divider, + ":before": { + backgroundColor: "transparent", + }, + "&:not(:last-of-type)": { + borderBottom: "none", + }, + "&:first-of-type": { + borderTopLeftRadius: (theme.vars || theme).shape.borderRadius, + borderTopRightRadius: (theme.vars || theme).shape.borderRadius, + }, + "&:last-of-type": { + borderBottomLeftRadius: (theme.vars || theme).shape.borderRadius, + borderBottomRightRadius: (theme.vars || theme).shape.borderRadius, + }, + }), + }, + }, + MuiAccordionSummary: { + styleOverrides: { + root: ({ theme }) => ({ + border: "none", + borderRadius: 8, + "&:hover": { backgroundColor: gray[50] }, + "&:focus-visible": { backgroundColor: "transparent" }, + ...theme.applyStyles("dark", { + "&:hover": { backgroundColor: gray[800] }, + }), + }), + }, + }, + MuiAccordionDetails: { + styleOverrides: { + root: { mb: 20, border: "none" }, + }, + }, + MuiPaper: { + defaultProps: { + elevation: 0, + }, + }, + MuiCard: { + styleOverrides: { + root: ({ theme }) => { + return { + padding: 16, + gap: 16, + transition: "all 100ms ease", + backgroundColor: gray[50], + borderRadius: (theme.vars || theme).shape.borderRadius, + border: `1px solid ${(theme.vars || theme).palette.divider}`, + boxShadow: "none", + ...theme.applyStyles("dark", { + backgroundColor: gray[800], + }), + variants: [ + { + props: { + variant: "outlined", + }, + style: { + border: `1px solid ${(theme.vars || theme).palette.divider}`, + boxShadow: "none", + background: "hsl(0, 0%, 100%)", + ...theme.applyStyles("dark", { + background: alpha(gray[900], 0.4), + }), + }, + }, + ], + }; + }, + }, + }, + MuiCardContent: { + styleOverrides: { + root: { + padding: 0, + "&:last-child": { paddingBottom: 0 }, + }, + }, + }, + MuiCardHeader: { + styleOverrides: { + root: { + padding: 0, + }, + }, + }, + MuiCardActions: { + styleOverrides: { + root: { + padding: 0, + }, + }, + }, +}; diff --git a/matrixgw_frontend/src/theme/themePrimitives.ts b/matrixgw_frontend/src/theme/themePrimitives.ts new file mode 100644 index 0000000..1e95864 --- /dev/null +++ b/matrixgw_frontend/src/theme/themePrimitives.ts @@ -0,0 +1,414 @@ +import { + createTheme, + alpha, + type PaletteMode, + type Shadows, +} from "@mui/material/styles"; + +declare module "@mui/material/Paper" { + interface PaperPropsVariantOverrides { + highlighted: true; + } +} +declare module "@mui/material/styles" { + interface ColorRange { + 50: string; + 100: string; + 200: string; + 300: string; + 400: string; + 500: string; + 600: string; + 700: string; + 800: string; + 900: string; + } + + interface PaletteColor extends ColorRange {} + + interface Palette { + baseShadow: string; + } +} + +const defaultTheme = createTheme(); + +const customShadows: Shadows = [...defaultTheme.shadows]; + +export const brand = { + 50: "hsl(210, 100%, 95%)", + 100: "hsl(210, 100%, 92%)", + 200: "hsl(210, 100%, 80%)", + 300: "hsl(210, 100%, 65%)", + 400: "hsl(210, 98%, 48%)", + 500: "hsl(210, 98%, 42%)", + 600: "hsl(210, 98%, 55%)", + 700: "hsl(210, 100%, 35%)", + 800: "hsl(210, 100%, 16%)", + 900: "hsl(210, 100%, 21%)", +}; + +export const gray = { + 50: "hsl(220, 35%, 97%)", + 100: "hsl(220, 30%, 94%)", + 200: "hsl(220, 20%, 88%)", + 300: "hsl(220, 20%, 80%)", + 400: "hsl(220, 20%, 65%)", + 500: "hsl(220, 20%, 42%)", + 600: "hsl(220, 20%, 35%)", + 700: "hsl(220, 20%, 25%)", + 800: "hsl(220, 30%, 6%)", + 900: "hsl(220, 35%, 3%)", +}; + +export const green = { + 50: "hsl(120, 80%, 98%)", + 100: "hsl(120, 75%, 94%)", + 200: "hsl(120, 75%, 87%)", + 300: "hsl(120, 61%, 77%)", + 400: "hsl(120, 44%, 53%)", + 500: "hsl(120, 59%, 30%)", + 600: "hsl(120, 70%, 25%)", + 700: "hsl(120, 75%, 16%)", + 800: "hsl(120, 84%, 10%)", + 900: "hsl(120, 87%, 6%)", +}; + +export const orange = { + 50: "hsl(45, 100%, 97%)", + 100: "hsl(45, 92%, 90%)", + 200: "hsl(45, 94%, 80%)", + 300: "hsl(45, 90%, 65%)", + 400: "hsl(45, 90%, 40%)", + 500: "hsl(45, 90%, 35%)", + 600: "hsl(45, 91%, 25%)", + 700: "hsl(45, 94%, 20%)", + 800: "hsl(45, 95%, 16%)", + 900: "hsl(45, 93%, 12%)", +}; + +export const red = { + 50: "hsl(0, 100%, 97%)", + 100: "hsl(0, 92%, 90%)", + 200: "hsl(0, 94%, 80%)", + 300: "hsl(0, 90%, 65%)", + 400: "hsl(0, 90%, 40%)", + 500: "hsl(0, 90%, 30%)", + 600: "hsl(0, 91%, 25%)", + 700: "hsl(0, 94%, 18%)", + 800: "hsl(0, 95%, 12%)", + 900: "hsl(0, 93%, 6%)", +}; + +export const getDesignTokens = (mode: PaletteMode) => { + customShadows[1] = + mode === "dark" + ? "hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px" + : "hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px"; + + return { + palette: { + mode, + primary: { + light: brand[200], + main: brand[400], + dark: brand[700], + contrastText: brand[50], + ...(mode === "dark" && { + contrastText: brand[50], + light: brand[300], + main: brand[400], + dark: brand[700], + }), + }, + info: { + light: brand[100], + main: brand[300], + dark: brand[600], + contrastText: gray[50], + ...(mode === "dark" && { + contrastText: brand[300], + light: brand[500], + main: brand[700], + dark: brand[900], + }), + }, + warning: { + light: orange[300], + main: orange[400], + dark: orange[800], + ...(mode === "dark" && { + light: orange[400], + main: orange[500], + dark: orange[700], + }), + }, + error: { + light: red[300], + main: red[400], + dark: red[800], + ...(mode === "dark" && { + light: red[400], + main: red[500], + dark: red[700], + }), + }, + success: { + light: green[300], + main: green[400], + dark: green[800], + ...(mode === "dark" && { + light: green[400], + main: green[500], + dark: green[700], + }), + }, + grey: { + ...gray, + }, + divider: mode === "dark" ? alpha(gray[700], 0.6) : alpha(gray[300], 0.4), + background: { + default: "hsl(0, 0%, 99%)", + paper: "hsl(220, 35%, 97%)", + ...(mode === "dark" && { + default: gray[900], + paper: "hsl(220, 30%, 7%)", + }), + }, + text: { + primary: gray[800], + secondary: gray[600], + warning: orange[400], + ...(mode === "dark" && { + primary: "hsl(0, 0%, 100%)", + secondary: gray[400], + }), + }, + action: { + hover: alpha(gray[200], 0.2), + selected: `${alpha(gray[200], 0.3)}`, + ...(mode === "dark" && { + hover: alpha(gray[600], 0.2), + selected: alpha(gray[600], 0.3), + }), + }, + }, + typography: { + fontFamily: "Inter, sans-serif", + h1: { + fontSize: defaultTheme.typography.pxToRem(48), + fontWeight: 600, + lineHeight: 1.2, + letterSpacing: -0.5, + }, + h2: { + fontSize: defaultTheme.typography.pxToRem(36), + fontWeight: 600, + lineHeight: 1.2, + }, + h3: { + fontSize: defaultTheme.typography.pxToRem(30), + lineHeight: 1.2, + }, + h4: { + fontSize: defaultTheme.typography.pxToRem(24), + fontWeight: 600, + lineHeight: 1.5, + }, + h5: { + fontSize: defaultTheme.typography.pxToRem(20), + fontWeight: 600, + }, + h6: { + fontSize: defaultTheme.typography.pxToRem(18), + fontWeight: 600, + }, + subtitle1: { + fontSize: defaultTheme.typography.pxToRem(18), + }, + subtitle2: { + fontSize: defaultTheme.typography.pxToRem(14), + fontWeight: 500, + }, + body1: { + fontSize: defaultTheme.typography.pxToRem(14), + }, + body2: { + fontSize: defaultTheme.typography.pxToRem(14), + fontWeight: 400, + }, + caption: { + fontSize: defaultTheme.typography.pxToRem(12), + fontWeight: 400, + }, + }, + shape: { + borderRadius: 8, + }, + shadows: customShadows, + }; +}; + +export const colorSchemes = { + light: { + palette: { + primary: { + light: brand[200], + main: brand[400], + dark: brand[700], + contrastText: brand[50], + }, + info: { + light: brand[100], + main: brand[300], + dark: brand[600], + contrastText: gray[50], + }, + warning: { + light: orange[300], + main: orange[400], + dark: orange[800], + }, + error: { + light: red[300], + main: red[400], + dark: red[800], + }, + success: { + light: green[300], + main: green[400], + dark: green[800], + }, + grey: { + ...gray, + }, + divider: alpha(gray[300], 0.4), + background: { + default: "hsl(0, 0%, 99%)", + paper: "hsl(220, 35%, 97%)", + }, + text: { + primary: gray[800], + secondary: gray[600], + warning: orange[400], + }, + action: { + hover: alpha(gray[200], 0.2), + selected: `${alpha(gray[200], 0.3)}`, + }, + baseShadow: + "hsla(220, 30%, 5%, 0.07) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.07) 0px 8px 16px -5px", + }, + }, + dark: { + palette: { + primary: { + contrastText: brand[50], + light: brand[300], + main: brand[400], + dark: brand[700], + }, + info: { + contrastText: brand[300], + light: brand[500], + main: brand[700], + dark: brand[900], + }, + warning: { + light: orange[400], + main: orange[500], + dark: orange[700], + }, + error: { + light: red[400], + main: red[500], + dark: red[700], + }, + success: { + light: green[400], + main: green[500], + dark: green[700], + }, + grey: { + ...gray, + }, + divider: alpha(gray[700], 0.6), + background: { + default: gray[900], + paper: "hsl(220, 30%, 7%)", + }, + text: { + primary: "hsl(0, 0%, 100%)", + secondary: gray[400], + }, + action: { + hover: alpha(gray[600], 0.2), + selected: alpha(gray[600], 0.3), + }, + baseShadow: + "hsla(220, 30%, 5%, 0.7) 0px 4px 16px 0px, hsla(220, 25%, 10%, 0.8) 0px 8px 16px -5px", + }, + }, +}; + +export const typography = { + fontFamily: "Inter, sans-serif", + h1: { + fontSize: defaultTheme.typography.pxToRem(48), + fontWeight: 600, + lineHeight: 1.2, + letterSpacing: -0.5, + }, + h2: { + fontSize: defaultTheme.typography.pxToRem(36), + fontWeight: 600, + lineHeight: 1.2, + }, + h3: { + fontSize: defaultTheme.typography.pxToRem(30), + lineHeight: 1.2, + }, + h4: { + fontSize: defaultTheme.typography.pxToRem(24), + fontWeight: 600, + lineHeight: 1.5, + }, + h5: { + fontSize: defaultTheme.typography.pxToRem(20), + fontWeight: 600, + }, + h6: { + fontSize: defaultTheme.typography.pxToRem(18), + fontWeight: 600, + }, + subtitle1: { + fontSize: defaultTheme.typography.pxToRem(18), + }, + subtitle2: { + fontSize: defaultTheme.typography.pxToRem(14), + fontWeight: 500, + }, + body1: { + fontSize: defaultTheme.typography.pxToRem(14), + }, + body2: { + fontSize: defaultTheme.typography.pxToRem(14), + fontWeight: 400, + }, + caption: { + fontSize: defaultTheme.typography.pxToRem(12), + fontWeight: 400, + }, +}; + +export const shape = { + borderRadius: 8, +}; + +// @ts-ignore +const defaultShadows: Shadows = [ + "none", + "var(--template-palette-baseShadow)", + ...defaultTheme.shadows.slice(2), +]; +export const shadows = defaultShadows; diff --git a/matrixgw_frontend/src/widgets/AsyncWidget.tsx b/matrixgw_frontend/src/widgets/AsyncWidget.tsx index d642a7f..bc7e036 100644 --- a/matrixgw_frontend/src/widgets/AsyncWidget.tsx +++ b/matrixgw_frontend/src/widgets/AsyncWidget.tsx @@ -50,10 +50,6 @@ export function AsyncWidget(p: { height: "100%", flex: "1", flexDirection: "column", - backgroundColor: (theme) => - theme.palette.mode === "light" - ? theme.palette.grey[100] - : theme.palette.grey[900], }} > - theme.palette.mode === "light" - ? theme.palette.grey[100] - : theme.palette.grey[900], }} > From 9ed711777c11e16c524685a8903c742ff26dfbc8 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 4 Nov 2025 19:43:51 +0100 Subject: [PATCH 008/124] Add webapp skeletons --- matrixgw_frontend/package-lock.json | 40 +++++++++++- matrixgw_frontend/package.json | 3 +- matrixgw_frontend/src/App.tsx | 62 ++++++++++++++++++- matrixgw_frontend/src/routes/HomeRoute.tsx | 3 + .../src/routes/NotFoundRoute.tsx | 23 +++++++ .../src/routes/auth/LoginRoute.tsx | 3 + .../src/routes/auth/OIDCCbRoute.tsx | 53 ++++++++++++++++ .../src/widgets/BaseAuthenticatedPage.tsx | 3 + matrixgw_frontend/src/widgets/RouterLink.tsx | 16 +++++ .../src/widgets/auth/AuthSingleMessage.tsx | 13 ++++ .../src/widgets/auth/BaseLoginPage.tsx | 3 + 11 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 matrixgw_frontend/src/routes/HomeRoute.tsx create mode 100644 matrixgw_frontend/src/routes/NotFoundRoute.tsx create mode 100644 matrixgw_frontend/src/routes/auth/LoginRoute.tsx create mode 100644 matrixgw_frontend/src/routes/auth/OIDCCbRoute.tsx create mode 100644 matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx create mode 100644 matrixgw_frontend/src/widgets/RouterLink.tsx create mode 100644 matrixgw_frontend/src/widgets/auth/AuthSingleMessage.tsx create mode 100644 matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index 5f2de0a..e80d450 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -14,7 +14,8 @@ "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "react-router": "^7.9.5" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -2012,6 +2013,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -3399,6 +3409,28 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.9.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", + "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -3536,6 +3568,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index 2025306..3207e3c 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -16,7 +16,8 @@ "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "react-router": "^7.9.5" }, "devDependencies": { "@eslint/js": "^9.36.0", diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index 20351d8..4b0d983 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -1,3 +1,61 @@ -export function App(): React.ReactElement { - return <>hello world; +import React from "react"; +import { + createBrowserRouter, + createRoutesFromElements, + Route, + RouterProvider, +} from "react-router"; +import { AuthApi } from "./api/AuthApi"; +import { ServerApi } from "./api/ServerApi"; +import { LoginRoute } from "./routes/auth/LoginRoute"; +import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; +import { HomeRoute } from "./routes/HomeRoute"; +import { NotFoundRoute } from "./routes/NotFoundRoute"; +import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; +import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; + +interface AuthContext { + signedIn: boolean; + setSignedIn: (signedIn: boolean) => void; +} + +const AuthContextK = React.createContext(null); + +export function App(): React.ReactElement { + const [signedIn, setSignedIn] = React.useState(AuthApi.SignedIn); + + const context: AuthContext = { + signedIn: signedIn, + setSignedIn: (s) => { + setSignedIn(s); + location.reload(); + }, + }; + + const router = createBrowserRouter( + createRoutesFromElements( + signedIn || ServerApi.Config.auth_disabled ? ( + }> + } /> + } /> + + ) : ( + }> + } /> + } /> + } /> + + ) + ) + ); + + return ( + + + + ); +} + +export function useAuth(): AuthContext { + return React.use(AuthContextK)!; } diff --git a/matrixgw_frontend/src/routes/HomeRoute.tsx b/matrixgw_frontend/src/routes/HomeRoute.tsx new file mode 100644 index 0000000..b93b24f --- /dev/null +++ b/matrixgw_frontend/src/routes/HomeRoute.tsx @@ -0,0 +1,3 @@ +export function HomeRoute(): React.ReactElement { + return

Todo home route

; +} diff --git a/matrixgw_frontend/src/routes/NotFoundRoute.tsx b/matrixgw_frontend/src/routes/NotFoundRoute.tsx new file mode 100644 index 0000000..72812e6 --- /dev/null +++ b/matrixgw_frontend/src/routes/NotFoundRoute.tsx @@ -0,0 +1,23 @@ +import { Button } from "@mui/material"; +import { RouterLink } from "../widgets/RouterLink"; + +export function NotFoundRoute(): React.ReactElement { + return ( +
+

404 Not found

+

The page you requested was not found!

+ + + +
+ ); +} diff --git a/matrixgw_frontend/src/routes/auth/LoginRoute.tsx b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx new file mode 100644 index 0000000..6190120 --- /dev/null +++ b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx @@ -0,0 +1,3 @@ +export function LoginRoute(): React.ReactElement { + return <>LoginRoute; +} diff --git a/matrixgw_frontend/src/routes/auth/OIDCCbRoute.tsx b/matrixgw_frontend/src/routes/auth/OIDCCbRoute.tsx new file mode 100644 index 0000000..90c67f8 --- /dev/null +++ b/matrixgw_frontend/src/routes/auth/OIDCCbRoute.tsx @@ -0,0 +1,53 @@ +import { CircularProgress } from "@mui/material"; +import { useEffect, useRef, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router"; +import { AuthApi } from "../../api/AuthApi"; +import { useAuth } from "../../App"; +import { AuthSingleMessage } from "../../widgets/auth/AuthSingleMessage"; + +/** + * OpenID login callback route + */ +export function OIDCCbRoute(): React.ReactElement { + const auth = useAuth(); + const navigate = useNavigate(); + + const [error, setError] = useState(false); + + const [searchParams] = useSearchParams(); + const code = searchParams.get("code"); + const state = searchParams.get("state"); + + const count = useRef(""); + + useEffect(() => { + const load = async () => { + try { + if (count.current === code) { + return; + } + count.current = code!; + + await AuthApi.FinishOpenIDLogin(code!, state!); + navigate("/"); + auth.setSignedIn(true); + } catch (e) { + console.error(e); + setError(true); + } + }; + + load(); + }); + + if (error) + return ( + + ); + + return ( + <> + + + ); +} diff --git a/matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx new file mode 100644 index 0000000..79607a4 --- /dev/null +++ b/matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx @@ -0,0 +1,3 @@ +export function BaseAuthenticatedPage(): React.ReactElement { + return

todo authenticated

; +} diff --git a/matrixgw_frontend/src/widgets/RouterLink.tsx b/matrixgw_frontend/src/widgets/RouterLink.tsx new file mode 100644 index 0000000..c946ed6 --- /dev/null +++ b/matrixgw_frontend/src/widgets/RouterLink.tsx @@ -0,0 +1,16 @@ +import { type PropsWithChildren } from "react"; +import { Link } from "react-router"; + +export function RouterLink( + p: PropsWithChildren<{ to: string; target?: React.HTMLAttributeAnchorTarget }> +): React.ReactElement { + return ( + + {p.children} + + ); +} diff --git a/matrixgw_frontend/src/widgets/auth/AuthSingleMessage.tsx b/matrixgw_frontend/src/widgets/auth/AuthSingleMessage.tsx new file mode 100644 index 0000000..02423df --- /dev/null +++ b/matrixgw_frontend/src/widgets/auth/AuthSingleMessage.tsx @@ -0,0 +1,13 @@ +import { Button } from "@mui/material"; +import { Link } from "react-router"; + +export function AuthSingleMessage(p: { message: string }): React.ReactElement { + return ( + <> +

{p.message}

+ + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx b/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx new file mode 100644 index 0000000..866dd64 --- /dev/null +++ b/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx @@ -0,0 +1,3 @@ +export function BaseLoginPage(): React.ReactElement { + return

Todo login page route

; +} From d9c96e85f700d9e60b145b3f1a908abbea153d46 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 4 Nov 2025 20:51:07 +0100 Subject: [PATCH 009/124] Authentication flow is functional --- matrixgw_frontend/index.html | 4 +- matrixgw_frontend/package-lock.json | 17 +++++ matrixgw_frontend/package.json | 2 + .../src/routes/auth/LoginRoute.tsx | 49 ++++++++++++- .../src/routes/auth/OIDCCbRoute.tsx | 4 +- .../src/widgets/auth/BaseLoginPage.tsx | 70 ++++++++++++++++++- 6 files changed, 140 insertions(+), 6 deletions(-) diff --git a/matrixgw_frontend/index.html b/matrixgw_frontend/index.html index 03c4e8f..5d7e649 100644 --- a/matrixgw_frontend/index.html +++ b/matrixgw_frontend/index.html @@ -2,9 +2,9 @@ - + - matrixgw_frontend + MatrixGW
diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index e80d450..152f32f 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -11,6 +11,8 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@fontsource/roboto": "^5.2.8", + "@mdi/js": "^7.4.47", + "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", "react": "^19.1.1", @@ -789,6 +791,21 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", + "license": "Apache-2.0" + }, + "node_modules/@mdi/react": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.6.1.tgz", + "integrity": "sha512-4qZeDcluDFGFTWkHs86VOlHkm6gnKaMql13/gpIcUQ8kzxHgpj31NuCkD8abECVfbULJ3shc7Yt4HJ6Wu6SN4w==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz", diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index 3207e3c..823193f 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -13,6 +13,8 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@fontsource/roboto": "^5.2.8", + "@mdi/js": "^7.4.47", + "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", "react": "^19.1.1", diff --git a/matrixgw_frontend/src/routes/auth/LoginRoute.tsx b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx index 6190120..75b9457 100644 --- a/matrixgw_frontend/src/routes/auth/LoginRoute.tsx +++ b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx @@ -1,3 +1,50 @@ +import { Alert, Box, Button, CircularProgress } from "@mui/material"; +import Icon from "@mdi/react"; +import { mdiOpenid } from "@mdi/js"; +import { ServerApi } from "../../api/ServerApi"; +import React from "react"; +import { AuthApi } from "../../api/AuthApi"; + export function LoginRoute(): React.ReactElement { - return <>LoginRoute; + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const authWithOpenID = async () => { + try { + setLoading(true); + + const res = await AuthApi.StartOpenIDLogin(); + window.location.href = res.url; + } catch (e) { + console.error(e); + setError("Failed to initialize OpenID login"); + } + }; + + if (loading) + return ( +
+ +
+ ); + + return ( + <> + {error && ( + + {error} + + )} + + + + + ); } diff --git a/matrixgw_frontend/src/routes/auth/OIDCCbRoute.tsx b/matrixgw_frontend/src/routes/auth/OIDCCbRoute.tsx index 90c67f8..82f80fe 100644 --- a/matrixgw_frontend/src/routes/auth/OIDCCbRoute.tsx +++ b/matrixgw_frontend/src/routes/auth/OIDCCbRoute.tsx @@ -46,8 +46,8 @@ export function OIDCCbRoute(): React.ReactElement { ); return ( - <> +
- +
); } diff --git a/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx b/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx index 866dd64..a6d86da 100644 --- a/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx +++ b/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx @@ -1,3 +1,71 @@ +import { mdiMessageTextFast } from "@mdi/js"; +import Icon from "@mdi/react"; +import { Typography } from "@mui/material"; +import MuiCard from "@mui/material/Card"; +import Stack from "@mui/material/Stack"; +import { styled } from "@mui/material/styles"; +import { Outlet } from "react-router"; + +const Card = styled(MuiCard)(({ theme }) => ({ + display: "flex", + flexDirection: "column", + alignSelf: "center", + width: "100%", + padding: theme.spacing(4), + gap: theme.spacing(2), + margin: "auto", + [theme.breakpoints.up("sm")]: { + maxWidth: "450px", + }, + boxShadow: + "hsla(220, 30%, 5%, 0.05) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.05) 0px 15px 35px -5px", + ...theme.applyStyles("dark", { + boxShadow: + "hsla(220, 30%, 5%, 0.5) 0px 5px 15px 0px, hsla(220, 25%, 10%, 0.08) 0px 15px 35px -5px", + }), +})); + +const SignInContainer = styled(Stack)(({ theme }) => ({ + height: "calc((1 - var(--template-frame-height, 0)) * 100dvh)", + minHeight: "100%", + padding: theme.spacing(2), + [theme.breakpoints.up("sm")]: { + padding: theme.spacing(4), + }, + "&::before": { + content: '""', + display: "block", + position: "absolute", + zIndex: -1, + inset: 0, + backgroundImage: + "radial-gradient(ellipse at 50% 50%, hsl(210, 100%, 97%), hsl(0, 0%, 100%))", + backgroundRepeat: "no-repeat", + ...theme.applyStyles("dark", { + backgroundImage: + "radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))", + }), + }, +})); + export function BaseLoginPage(): React.ReactElement { - return

Todo login page route

; + return ( + + + + {" "} + MatrixGW + + + + + ); } From fdcd565431e3e2c5815f741966311200de251040 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 4 Nov 2025 21:31:54 +0100 Subject: [PATCH 010/124] Add base authenticated route --- matrixgw_frontend/src/App.tsx | 2 +- .../src/widgets/BaseAuthenticatedPage.tsx | 3 - .../dashboard/BaseAuthenticatedPage.tsx | 96 ++++++ .../src/widgets/dashboard/DashboardHeader.tsx | 130 ++++++++ .../widgets/dashboard/DashboardSidebar.tsx | 281 ++++++++++++++++++ .../dashboard/DashboardSidebarContext.tsx | 5 + .../dashboard/DashboardSidebarDividerItem.tsx | 28 ++ .../dashboard/DashboardSidebarHeaderItem.tsx | 46 +++ .../dashboard/DashboardSidebarPageItem.tsx | 253 ++++++++++++++++ .../widgets/dashboard/SidebarDividerItem.tsx | 28 ++ .../src/widgets/dashboard/ThemeSwitcher.tsx | 59 ++++ .../src/widgets/dashboard/constants.ts | 2 + .../src/widgets/dashboard/mixins.ts | 23 ++ 13 files changed, 952 insertions(+), 4 deletions(-) delete mode 100644 matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebarDividerItem.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/SidebarDividerItem.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx create mode 100644 matrixgw_frontend/src/widgets/dashboard/constants.ts create mode 100644 matrixgw_frontend/src/widgets/dashboard/mixins.ts diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index 4b0d983..22acab2 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -11,8 +11,8 @@ import { LoginRoute } from "./routes/auth/LoginRoute"; import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; import { HomeRoute } from "./routes/HomeRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute"; -import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; +import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage"; interface AuthContext { signedIn: boolean; diff --git a/matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx deleted file mode 100644 index 79607a4..0000000 --- a/matrixgw_frontend/src/widgets/BaseAuthenticatedPage.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function BaseAuthenticatedPage(): React.ReactElement { - return

todo authenticated

; -} diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx new file mode 100644 index 0000000..7973152 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -0,0 +1,96 @@ +import { mdiMessageTextFast } from "@mdi/js"; +import Icon from "@mdi/react"; +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import Toolbar from "@mui/material/Toolbar"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import * as React from "react"; +import { Outlet } from "react-router"; +import DashboardHeader from "./DashboardHeader"; +import DashboardSidebar from "./DashboardSidebar"; + +export default function BaseAuthenticatedPage(): React.ReactElement { + const theme = useTheme(); + + const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] = + React.useState(true); + const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] = + React.useState(false); + + const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md")); + + const isNavigationExpanded = isOverMdViewport + ? isDesktopNavigationExpanded + : isMobileNavigationExpanded; + + const setIsNavigationExpanded = React.useCallback( + (newExpanded: boolean) => { + if (isOverMdViewport) { + setIsDesktopNavigationExpanded(newExpanded); + } else { + setIsMobileNavigationExpanded(newExpanded); + } + }, + [ + isOverMdViewport, + setIsDesktopNavigationExpanded, + setIsMobileNavigationExpanded, + ] + ); + + const handleToggleHeaderMenu = React.useCallback( + (isExpanded: boolean) => { + setIsNavigationExpanded(isExpanded); + }, + [setIsNavigationExpanded] + ); + + const layoutRef = React.useRef(null); + + return ( + + } + title="" + menuOpen={isNavigationExpanded} + onToggleMenu={handleToggleHeaderMenu} + /> + + + + + + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx new file mode 100644 index 0000000..5c98db7 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx @@ -0,0 +1,130 @@ +import * as React from "react"; +import { styled, useTheme } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import MuiAppBar from "@mui/material/AppBar"; +import IconButton from "@mui/material/IconButton"; +import Toolbar from "@mui/material/Toolbar"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import MenuIcon from "@mui/icons-material/Menu"; +import MenuOpenIcon from "@mui/icons-material/MenuOpen"; +import Stack from "@mui/material/Stack"; +import { Link } from "react-router"; +import ThemeSwitcher from "./ThemeSwitcher"; + +const AppBar = styled(MuiAppBar)(({ theme }) => ({ + borderWidth: 0, + borderBottomWidth: 1, + borderStyle: "solid", + borderColor: (theme.vars ?? theme).palette.divider, + boxShadow: "none", + zIndex: theme.zIndex.drawer + 1, +})); + +const LogoContainer = styled("div")({ + position: "relative", + height: 40, + display: "flex", + alignItems: "center", + "& img": { + maxHeight: 40, + }, +}); + +export interface DashboardHeaderProps { + logo?: React.ReactNode; + title?: string; + menuOpen: boolean; + onToggleMenu: (open: boolean) => void; +} + +export default function DashboardHeader({ + logo, + title, + menuOpen, + onToggleMenu, +}: DashboardHeaderProps) { + const theme = useTheme(); + + const handleMenuOpen = React.useCallback(() => { + onToggleMenu(!menuOpen); + }, [menuOpen, onToggleMenu]); + + const getMenuIcon = React.useCallback( + (isExpanded: boolean) => { + const expandMenuActionText = "Expand"; + const collapseMenuActionText = "Collapse"; + + return ( + +
+ + {isExpanded ? : } + +
+
+ ); + }, + [handleMenuOpen] + ); + + return ( + + + + + {getMenuIcon(menuOpen)} + + + {logo ? {logo} : null} + {title ? ( + + {title} + + ) : null} + + + + + + + + + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx new file mode 100644 index 0000000..26111f6 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx @@ -0,0 +1,281 @@ +import * as React from "react"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import Box from "@mui/material/Box"; +import Drawer from "@mui/material/Drawer"; +import List from "@mui/material/List"; +import Toolbar from "@mui/material/Toolbar"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import PersonIcon from "@mui/icons-material/Person"; +import BarChartIcon from "@mui/icons-material/BarChart"; +import DescriptionIcon from "@mui/icons-material/Description"; +import LayersIcon from "@mui/icons-material/Layers"; +import { matchPath, useLocation } from "react-router"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { DRAWER_WIDTH, MINI_DRAWER_WIDTH } from "./constants"; +import DashboardSidebarPageItem from "./DashboardSidebarPageItem"; +import DashboardSidebarHeaderItem from "./DashboardSidebarHeaderItem"; +import DashboardSidebarDividerItem from "./DashboardSidebarDividerItem"; +import { + getDrawerSxTransitionMixin, + getDrawerWidthTransitionMixin, +} from "./mixins"; + +export interface DashboardSidebarProps { + expanded?: boolean; + setExpanded: (expanded: boolean) => void; + disableCollapsibleSidebar?: boolean; + container?: Element; +} + +export default function DashboardSidebar({ + expanded = true, + setExpanded, + disableCollapsibleSidebar = false, + container, +}: DashboardSidebarProps) { + const theme = useTheme(); + + const { pathname } = useLocation(); + + const [expandedItemIds, setExpandedItemIds] = React.useState([]); + + const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm")); + const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md")); + + const [isFullyExpanded, setIsFullyExpanded] = React.useState(expanded); + const [isFullyCollapsed, setIsFullyCollapsed] = React.useState(!expanded); + + React.useEffect(() => { + if (expanded) { + const drawerWidthTransitionTimeout = setTimeout(() => { + setIsFullyExpanded(true); + }, theme.transitions.duration.enteringScreen); + + return () => clearTimeout(drawerWidthTransitionTimeout); + } + + setIsFullyExpanded(false); + + return () => {}; + }, [expanded, theme.transitions.duration.enteringScreen]); + + React.useEffect(() => { + if (!expanded) { + const drawerWidthTransitionTimeout = setTimeout(() => { + setIsFullyCollapsed(true); + }, theme.transitions.duration.leavingScreen); + + return () => clearTimeout(drawerWidthTransitionTimeout); + } + + setIsFullyCollapsed(false); + + return () => {}; + }, [expanded, theme.transitions.duration.leavingScreen]); + + const mini = !disableCollapsibleSidebar && !expanded; + + const handleSetSidebarExpanded = React.useCallback( + (newExpanded: boolean) => () => { + setExpanded(newExpanded); + }, + [setExpanded] + ); + + const handlePageItemClick = React.useCallback( + (itemId: string, hasNestedNavigation: boolean) => { + if (hasNestedNavigation && !mini) { + setExpandedItemIds((previousValue) => + previousValue.includes(itemId) + ? previousValue.filter( + (previousValueItemId) => previousValueItemId !== itemId + ) + : [...previousValue, itemId] + ); + } else if (!isOverSmViewport && !hasNestedNavigation) { + setExpanded(false); + } + }, + [mini, setExpanded, isOverSmViewport] + ); + + const hasDrawerTransitions = + isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport); + + const getDrawerContent = React.useCallback( + (viewport: "phone" | "tablet" | "desktop") => ( + + + + + Main items + } + href="/employees" + selected={ + !!matchPath("/employees/*", pathname) || pathname === "/" + } + /> + + + Example items + + } + href="/reports" + selected={!!matchPath("/reports", pathname)} + defaultExpanded={!!matchPath("/reports", pathname)} + expanded={expandedItemIds.includes("reports")} + nestedNavigation={ + + } + href="/reports/sales" + selected={!!matchPath("/reports/sales", pathname)} + /> + } + href="/reports/traffic" + selected={!!matchPath("/reports/traffic", pathname)} + /> + + } + /> + } + href="/integrations" + selected={!!matchPath("/integrations", pathname)} + /> + + + + ), + [mini, hasDrawerTransitions, isFullyExpanded, expandedItemIds, pathname] + ); + + const getDrawerSharedSx = React.useCallback( + (isTemporary: boolean) => { + const drawerWidth = mini ? MINI_DRAWER_WIDTH : DRAWER_WIDTH; + + return { + displayPrint: "none", + width: drawerWidth, + flexShrink: 0, + ...getDrawerWidthTransitionMixin(expanded), + ...(isTemporary ? { position: "absolute" } : {}), + [`& .MuiDrawer-paper`]: { + position: "absolute", + width: drawerWidth, + boxSizing: "border-box", + backgroundImage: "none", + ...getDrawerWidthTransitionMixin(expanded), + }, + }; + }, + [expanded, mini] + ); + + const sidebarContextValue = React.useMemo(() => { + return { + onPageItemClick: handlePageItemClick, + mini, + fullyExpanded: isFullyExpanded, + fullyCollapsed: isFullyCollapsed, + hasDrawerTransitions, + }; + }, [ + handlePageItemClick, + mini, + isFullyExpanded, + isFullyCollapsed, + hasDrawerTransitions, + ]); + + return ( + + + {getDrawerContent("phone")} + + + {getDrawerContent("tablet")} + + + {getDrawerContent("desktop")} + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx new file mode 100644 index 0000000..56efe06 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx @@ -0,0 +1,5 @@ +import * as React from "react"; + +const DashboardSidebarContext = React.createContext(null); + +export default DashboardSidebarContext; diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarDividerItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarDividerItem.tsx new file mode 100644 index 0000000..d5e3178 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarDividerItem.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import Divider from "@mui/material/Divider"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { getDrawerSxTransitionMixin } from "./mixins"; + +export default function DashboardSidebarDividerItem() { + const sidebarContext = React.useContext(DashboardSidebarContext); + if (!sidebarContext) { + throw new Error("Sidebar context was used without a provider."); + } + const { fullyExpanded = true, hasDrawerTransitions } = sidebarContext; + + return ( +
  • + +
  • + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx new file mode 100644 index 0000000..d4dc3ce --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import ListSubheader from "@mui/material/ListSubheader"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { DRAWER_WIDTH } from "./constants"; +import { getDrawerSxTransitionMixin } from "./mixins"; + +export interface DashboardSidebarHeaderItemProps { + children?: React.ReactNode; +} + +export default function DashboardSidebarHeaderItem({ + children, +}: DashboardSidebarHeaderItemProps) { + const sidebarContext = React.useContext(DashboardSidebarContext); + if (!sidebarContext) { + throw new Error("Sidebar context was used without a provider."); + } + const { + mini = false, + fullyExpanded = true, + hasDrawerTransitions, + } = sidebarContext; + + return ( + + {children} + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx new file mode 100644 index 0000000..637db5b --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx @@ -0,0 +1,253 @@ +import * as React from "react"; +import { type Theme, type SxProps } from "@mui/material/styles"; +import Avatar from "@mui/material/Avatar"; +import Box from "@mui/material/Box"; +import Collapse from "@mui/material/Collapse"; +import Grow from "@mui/material/Grow"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Paper from "@mui/material/Paper"; +import Typography from "@mui/material/Typography"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import { Link } from "react-router"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { MINI_DRAWER_WIDTH } from "./constants"; + +export interface DashboardSidebarPageItemProps { + id: string; + title: string; + icon?: React.ReactNode; + href: string; + action?: React.ReactNode; + defaultExpanded?: boolean; + expanded?: boolean; + selected?: boolean; + disabled?: boolean; + nestedNavigation?: React.ReactNode; +} + +export default function DashboardSidebarPageItem({ + id, + title, + icon, + href, + action, + defaultExpanded = false, + expanded = defaultExpanded, + selected = false, + disabled = false, + nestedNavigation, +}: DashboardSidebarPageItemProps) { + const sidebarContext = React.useContext(DashboardSidebarContext); + if (!sidebarContext) { + throw new Error("Sidebar context was used without a provider."); + } + const { + onPageItemClick, + mini = false, + fullyExpanded = true, + fullyCollapsed = false, + } = sidebarContext; + + const [isHovered, setIsHovered] = React.useState(false); + + const handleClick = React.useCallback(() => { + if (onPageItemClick) { + onPageItemClick(id, !!nestedNavigation); + } + }, [onPageItemClick, id, nestedNavigation]); + + let nestedNavigationCollapseSx: SxProps = { display: "none" }; + if (mini && fullyCollapsed) { + nestedNavigationCollapseSx = { + fontSize: 18, + position: "absolute", + top: "41.5%", + right: "2px", + transform: "translateY(-50%) rotate(-90deg)", + }; + } else if (!mini && fullyExpanded) { + nestedNavigationCollapseSx = { + ml: 0.5, + fontSize: 20, + transform: `rotate(${expanded ? 0 : -90}deg)`, + transition: (theme: Theme) => + theme.transitions.create("transform", { + easing: theme.transitions.easing.sharp, + duration: 100, + }), + }; + } + + const hasExternalHref = href + ? href.startsWith("http://") || href.startsWith("https://") + : false; + + const LinkComponent = hasExternalHref ? "a" : Link; + + const miniNestedNavigationSidebarContextValue = React.useMemo(() => { + return { + onPageItemClick: onPageItemClick ?? (() => {}), + mini: false, + fullyExpanded: true, + fullyCollapsed: false, + hasDrawerTransitions: false, + }; + }, [onPageItemClick]); + + return ( + + { + setIsHovered(true); + }, + onMouseLeave: () => { + setIsHovered(false); + }, + } + : {})} + sx={{ + display: "block", + py: 0, + px: 1, + overflowX: "hidden", + }} + > + + {icon || mini ? ( + + + {icon ?? null} + {!icon && mini ? ( + + {title + .split(" ") + .slice(0, 2) + .map((titleWord) => titleWord.charAt(0).toUpperCase())} + + ) : null} + + {mini ? ( + + {title} + + ) : null} + + ) : null} + {!mini ? ( + + ) : null} + {action && !mini && fullyExpanded ? action : null} + {nestedNavigation ? ( + + ) : null} + + {nestedNavigation && mini ? ( + + + + + {nestedNavigation} + + + + + ) : null} + + {nestedNavigation && !mini ? ( + + {nestedNavigation} + + ) : null} + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/SidebarDividerItem.tsx b/matrixgw_frontend/src/widgets/dashboard/SidebarDividerItem.tsx new file mode 100644 index 0000000..d5e3178 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/SidebarDividerItem.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import Divider from "@mui/material/Divider"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { getDrawerSxTransitionMixin } from "./mixins"; + +export default function DashboardSidebarDividerItem() { + const sidebarContext = React.useContext(DashboardSidebarContext); + if (!sidebarContext) { + throw new Error("Sidebar context was used without a provider."); + } + const { fullyExpanded = true, hasDrawerTransitions } = sidebarContext; + + return ( +
  • + +
  • + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx b/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx new file mode 100644 index 0000000..0fd38c6 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { useTheme, useColorScheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import IconButton from "@mui/material/IconButton"; +import Tooltip from "@mui/material/Tooltip"; +import DarkModeIcon from "@mui/icons-material/DarkMode"; +import LightModeIcon from "@mui/icons-material/LightMode"; +import type {} from "@mui/material/themeCssVarsAugmentation"; + +export default function ThemeSwitcher() { + const theme = useTheme(); + + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); + const preferredMode = prefersDarkMode ? "dark" : "light"; + + const { mode, setMode } = useColorScheme(); + + const paletteMode = !mode || mode === "system" ? preferredMode : mode; + + const toggleMode = React.useCallback(() => { + setMode(paletteMode === "dark" ? "light" : "dark"); + }, [setMode, paletteMode]); + + return ( + +
    + + + + + + +
    +
    + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/constants.ts b/matrixgw_frontend/src/widgets/dashboard/constants.ts new file mode 100644 index 0000000..780f97a --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/constants.ts @@ -0,0 +1,2 @@ +export const DRAWER_WIDTH = 240; // px +export const MINI_DRAWER_WIDTH = 90; // px diff --git a/matrixgw_frontend/src/widgets/dashboard/mixins.ts b/matrixgw_frontend/src/widgets/dashboard/mixins.ts new file mode 100644 index 0000000..bee5cd9 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/mixins.ts @@ -0,0 +1,23 @@ +import { type Theme } from "@mui/material/styles"; + +export function getDrawerSxTransitionMixin( + isExpanded: boolean, + property: string +) { + return { + transition: (theme: Theme) => + theme.transitions.create(property, { + easing: theme.transitions.easing.sharp, + duration: isExpanded + ? theme.transitions.duration.enteringScreen + : theme.transitions.duration.leavingScreen, + }), + }; +} + +export function getDrawerWidthTransitionMixin(isExpanded: boolean) { + return { + ...getDrawerSxTransitionMixin(isExpanded, "width"), + overflowX: "hidden", + }; +} From 79b5a767f3780263341eabcdb09becc72e091bea Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 4 Nov 2025 21:51:20 +0100 Subject: [PATCH 011/124] Improve sidebar --- .../dashboard/BaseAuthenticatedPage.tsx | 4 - .../src/widgets/dashboard/DashboardHeader.tsx | 59 ++++---- .../widgets/dashboard/DashboardSidebar.tsx | 84 ++--------- .../dashboard/DashboardSidebarContext.tsx | 8 +- .../dashboard/DashboardSidebarHeaderItem.tsx | 46 ------ .../dashboard/DashboardSidebarPageItem.tsx | 141 +++--------------- 6 files changed, 68 insertions(+), 274 deletions(-) delete mode 100644 matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index 7973152..ff3ee64 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -1,5 +1,3 @@ -import { mdiMessageTextFast } from "@mdi/js"; -import Icon from "@mdi/react"; import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import Toolbar from "@mui/material/Toolbar"; @@ -59,8 +57,6 @@ export default function BaseAuthenticatedPage(): React.ReactElement { }} > } - title="" menuOpen={isNavigationExpanded} onToggleMenu={handleToggleHeaderMenu} /> diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx index 5c98db7..cf669b9 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx @@ -1,15 +1,17 @@ -import * as React from "react"; -import { styled, useTheme } from "@mui/material/styles"; -import Box from "@mui/material/Box"; +import { mdiMessageTextFast } from "@mdi/js"; +import Icon from "@mdi/react"; +import MenuIcon from "@mui/icons-material/Menu"; +import MenuOpenIcon from "@mui/icons-material/MenuOpen"; import MuiAppBar from "@mui/material/AppBar"; +import Box from "@mui/material/Box"; import IconButton from "@mui/material/IconButton"; +import Stack from "@mui/material/Stack"; +import { styled } from "@mui/material/styles"; import Toolbar from "@mui/material/Toolbar"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; -import MenuIcon from "@mui/icons-material/Menu"; -import MenuOpenIcon from "@mui/icons-material/MenuOpen"; -import Stack from "@mui/material/Stack"; -import { Link } from "react-router"; +import * as React from "react"; +import { RouterLink } from "../RouterLink"; import ThemeSwitcher from "./ThemeSwitcher"; const AppBar = styled(MuiAppBar)(({ theme }) => ({ @@ -32,20 +34,14 @@ const LogoContainer = styled("div")({ }); export interface DashboardHeaderProps { - logo?: React.ReactNode; - title?: string; menuOpen: boolean; onToggleMenu: (open: boolean) => void; } export default function DashboardHeader({ - logo, - title, menuOpen, onToggleMenu, }: DashboardHeaderProps) { - const theme = useTheme(); - const handleMenuOpen = React.useCallback(() => { onToggleMenu(!menuOpen); }, [menuOpen, onToggleMenu]); @@ -60,7 +56,7 @@ export default function DashboardHeader({ title={`${ isExpanded ? collapseMenuActionText : expandMenuActionText } menu`} - enterDelay={1000} + enterDelay={200} >
    - {getMenuIcon(menuOpen)} - + {getMenuIcon(menuOpen)} + - {logo ? {logo} : null} - {title ? ( - - {title} - - ) : null} + + + + + MatrixGW + - + ([]); - const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm")); const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md")); @@ -83,22 +77,11 @@ export default function DashboardSidebar({ [setExpanded] ); - const handlePageItemClick = React.useCallback( - (itemId: string, hasNestedNavigation: boolean) => { - if (hasNestedNavigation && !mini) { - setExpandedItemIds((previousValue) => - previousValue.includes(itemId) - ? previousValue.filter( - (previousValueItemId) => previousValueItemId !== itemId - ) - : [...previousValue, itemId] - ); - } else if (!isOverSmViewport && !hasNestedNavigation) { - setExpanded(false); - } - }, - [mini, setExpanded, isOverSmViewport] - ); + const handlePageItemClick = React.useCallback(() => { + if (!isOverSmViewport) { + setExpanded(false); + } + }, [mini, setExpanded, isOverSmViewport]); const hasDrawerTransitions = isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport); @@ -132,67 +115,30 @@ export default function DashboardSidebar({ width: mini ? MINI_DRAWER_WIDTH : "auto", }} > - Main items } href="/employees" - selected={ - !!matchPath("/employees/*", pathname) || pathname === "/" - } /> - - Example items - } href="/reports" - selected={!!matchPath("/reports", pathname)} - defaultExpanded={!!matchPath("/reports", pathname)} - expanded={expandedItemIds.includes("reports")} - nestedNavigation={ - - } - href="/reports/sales" - selected={!!matchPath("/reports/sales", pathname)} - /> - } - href="/reports/traffic" - selected={!!matchPath("/reports/traffic", pathname)} - /> - - } /> } href="/integrations" - selected={!!matchPath("/integrations", pathname)} /> ), - [mini, hasDrawerTransitions, isFullyExpanded, expandedItemIds, pathname] + [mini, hasDrawerTransitions, isFullyExpanded] ); const getDrawerSharedSx = React.useCallback( diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx index 56efe06..eb0dafd 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx @@ -1,5 +1,11 @@ import * as React from "react"; -const DashboardSidebarContext = React.createContext(null); +const DashboardSidebarContext = React.createContext<{ + onPageItemClick: () => void; + mini: boolean; + fullyExpanded: boolean; + fullyCollapsed: boolean; + hasDrawerTransitions: boolean; +} | null>(null); export default DashboardSidebarContext; diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx deleted file mode 100644 index d4dc3ce..0000000 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarHeaderItem.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react"; -import ListSubheader from "@mui/material/ListSubheader"; -import type {} from "@mui/material/themeCssVarsAugmentation"; -import DashboardSidebarContext from "./DashboardSidebarContext"; -import { DRAWER_WIDTH } from "./constants"; -import { getDrawerSxTransitionMixin } from "./mixins"; - -export interface DashboardSidebarHeaderItemProps { - children?: React.ReactNode; -} - -export default function DashboardSidebarHeaderItem({ - children, -}: DashboardSidebarHeaderItemProps) { - const sidebarContext = React.useContext(DashboardSidebarContext); - if (!sidebarContext) { - throw new Error("Sidebar context was used without a provider."); - } - const { - mini = false, - fullyExpanded = true, - hasDrawerTransitions, - } = sidebarContext; - - return ( - - {children} - - ); -} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx index 637db5b..75eb1b4 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx @@ -1,18 +1,13 @@ -import * as React from "react"; -import { type Theme, type SxProps } from "@mui/material/styles"; import Avatar from "@mui/material/Avatar"; import Box from "@mui/material/Box"; -import Collapse from "@mui/material/Collapse"; -import Grow from "@mui/material/Grow"; import ListItem from "@mui/material/ListItem"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemText from "@mui/material/ListItemText"; -import Paper from "@mui/material/Paper"; import Typography from "@mui/material/Typography"; import type {} from "@mui/material/themeCssVarsAugmentation"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import { Link } from "react-router"; +import * as React from "react"; +import { Link, matchPath, useLocation } from "react-router"; import DashboardSidebarContext from "./DashboardSidebarContext"; import { MINI_DRAWER_WIDTH } from "./constants"; @@ -22,11 +17,7 @@ export interface DashboardSidebarPageItemProps { icon?: React.ReactNode; href: string; action?: React.ReactNode; - defaultExpanded?: boolean; - expanded?: boolean; - selected?: boolean; disabled?: boolean; - nestedNavigation?: React.ReactNode; } export default function DashboardSidebarPageItem({ @@ -35,12 +26,10 @@ export default function DashboardSidebarPageItem({ icon, href, action, - defaultExpanded = false, - expanded = defaultExpanded, - selected = false, disabled = false, - nestedNavigation, }: DashboardSidebarPageItemProps) { + const { pathname } = useLocation(); + const sidebarContext = React.useContext(DashboardSidebarContext); if (!sidebarContext) { throw new Error("Sidebar context was used without a provider."); @@ -49,38 +38,13 @@ export default function DashboardSidebarPageItem({ onPageItemClick, mini = false, fullyExpanded = true, - fullyCollapsed = false, } = sidebarContext; - const [isHovered, setIsHovered] = React.useState(false); - const handleClick = React.useCallback(() => { if (onPageItemClick) { - onPageItemClick(id, !!nestedNavigation); + onPageItemClick(); } - }, [onPageItemClick, id, nestedNavigation]); - - let nestedNavigationCollapseSx: SxProps = { display: "none" }; - if (mini && fullyCollapsed) { - nestedNavigationCollapseSx = { - fontSize: 18, - position: "absolute", - top: "41.5%", - right: "2px", - transform: "translateY(-50%) rotate(-90deg)", - }; - } else if (!mini && fullyExpanded) { - nestedNavigationCollapseSx = { - ml: 0.5, - fontSize: 20, - transform: `rotate(${expanded ? 0 : -90}deg)`, - transition: (theme: Theme) => - theme.transitions.create("transform", { - easing: theme.transitions.easing.sharp, - duration: 100, - }), - }; - } + }, [onPageItemClick, id]); const hasExternalHref = href ? href.startsWith("http://") || href.startsWith("https://") @@ -88,61 +52,28 @@ export default function DashboardSidebarPageItem({ const LinkComponent = hasExternalHref ? "a" : Link; - const miniNestedNavigationSidebarContextValue = React.useMemo(() => { - return { - onPageItemClick: onPageItemClick ?? (() => {}), - mini: false, - fullyExpanded: true, - fullyCollapsed: false, - hasDrawerTransitions: false, - }; - }, [onPageItemClick]); + const selected = !!matchPath(href, pathname); return ( - { - setIsHovered(true); - }, - onMouseLeave: () => { - setIsHovered(false); - }, - } - : {})} - sx={{ - display: "block", - py: 0, - px: 1, - overflowX: "hidden", - }} - > + {icon || mini ? ( ) : null} {action && !mini && fullyExpanded ? action : null} - {nestedNavigation ? ( - - ) : null} - {nestedNavigation && mini ? ( - - - - - {nestedNavigation} - - - - - ) : null} - {nestedNavigation && !mini ? ( - - {nestedNavigation} - - ) : null} ); } From 3de26c0fffb6d15ea4641df14233205a86ea7702 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 4 Nov 2025 22:02:56 +0100 Subject: [PATCH 012/124] Simplify dashboard code --- .../dashboard/BaseAuthenticatedPage.tsx | 2 +- .../widgets/dashboard/DashboardSidebar.tsx | 56 ++++++------------- .../dashboard/DashboardSidebarContext.tsx | 1 - .../dashboard/DashboardSidebarPageItem.tsx | 12 +--- 4 files changed, 20 insertions(+), 51 deletions(-) diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index ff3ee64..60ae762 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -11,7 +11,7 @@ export default function BaseAuthenticatedPage(): React.ReactElement { const theme = useTheme(); const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] = - React.useState(true); + React.useState(false); const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] = React.useState(false); diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx index 7eee311..d1584a6 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx @@ -1,6 +1,5 @@ -import BarChartIcon from "@mui/icons-material/BarChart"; -import LayersIcon from "@mui/icons-material/Layers"; -import PersonIcon from "@mui/icons-material/Person"; +import { mdiBug, mdiForum, mdiKeyVariant, mdiLinkLock } from "@mdi/js"; +import Icon from "@mdi/react"; import Box from "@mui/material/Box"; import Drawer from "@mui/material/Drawer"; import List from "@mui/material/List"; @@ -9,7 +8,6 @@ import { useTheme } from "@mui/material/styles"; import type {} from "@mui/material/themeCssVarsAugmentation"; import useMediaQuery from "@mui/material/useMediaQuery"; import * as React from "react"; -import { useLocation } from "react-router"; import DashboardSidebarContext from "./DashboardSidebarContext"; import DashboardSidebarDividerItem from "./DashboardSidebarDividerItem"; import DashboardSidebarPageItem from "./DashboardSidebarPageItem"; @@ -38,7 +36,6 @@ export default function DashboardSidebar({ const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md")); const [isFullyExpanded, setIsFullyExpanded] = React.useState(expanded); - const [isFullyCollapsed, setIsFullyCollapsed] = React.useState(!expanded); React.useEffect(() => { if (expanded) { @@ -54,20 +51,6 @@ export default function DashboardSidebar({ return () => {}; }, [expanded, theme.transitions.duration.enteringScreen]); - React.useEffect(() => { - if (!expanded) { - const drawerWidthTransitionTimeout = setTimeout(() => { - setIsFullyCollapsed(true); - }, theme.transitions.duration.leavingScreen); - - return () => clearTimeout(drawerWidthTransitionTimeout); - } - - setIsFullyCollapsed(false); - - return () => {}; - }, [expanded, theme.transitions.duration.leavingScreen]); - const mini = !disableCollapsibleSidebar && !expanded; const handleSetSidebarExpanded = React.useCallback( @@ -116,23 +99,25 @@ export default function DashboardSidebar({ }} > } - href="/employees" + title="Messages" + icon={} + href="/" /> } - href="/reports" + title="Matrix link" + icon={} + href="/matrixlink" /> } - href="/integrations" + title="API tokens" + icon={} + href="/tokens" + /> + } + href="/wsdebug" /> @@ -168,16 +153,9 @@ export default function DashboardSidebar({ onPageItemClick: handlePageItemClick, mini, fullyExpanded: isFullyExpanded, - fullyCollapsed: isFullyCollapsed, hasDrawerTransitions, }; - }, [ - handlePageItemClick, - mini, - isFullyExpanded, - isFullyCollapsed, - hasDrawerTransitions, - ]); + }, [handlePageItemClick, mini, isFullyExpanded, hasDrawerTransitions]); return ( diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx index eb0dafd..751b63b 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx @@ -4,7 +4,6 @@ const DashboardSidebarContext = React.createContext<{ onPageItemClick: () => void; mini: boolean; fullyExpanded: boolean; - fullyCollapsed: boolean; hasDrawerTransitions: boolean; } | null>(null); diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx index 75eb1b4..5d32561 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx @@ -12,7 +12,6 @@ import DashboardSidebarContext from "./DashboardSidebarContext"; import { MINI_DRAWER_WIDTH } from "./constants"; export interface DashboardSidebarPageItemProps { - id: string; title: string; icon?: React.ReactNode; href: string; @@ -21,7 +20,6 @@ export interface DashboardSidebarPageItemProps { } export default function DashboardSidebarPageItem({ - id, title, icon, href, @@ -40,12 +38,6 @@ export default function DashboardSidebarPageItem({ fullyExpanded = true, } = sidebarContext; - const handleClick = React.useCallback(() => { - if (onPageItemClick) { - onPageItemClick(); - } - }, [onPageItemClick, id]); - const hasExternalHref = href ? href.startsWith("http://") || href.startsWith("https://") : false; @@ -56,7 +48,7 @@ export default function DashboardSidebarPageItem({ return ( - + {icon || mini ? ( From f9fb99cdb58623e0cd9cbc6caa114831c55f0099 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 4 Nov 2025 22:04:12 +0100 Subject: [PATCH 013/124] Add TODO marker --- matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx index cf669b9..44444bc 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx @@ -118,6 +118,7 @@ export default function DashboardHeader({ + Hi TODO USER SIGN OUT From a44327ddb00d7385196b8a22fe15eecb4b3017d2 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 5 Nov 2025 08:31:47 +0100 Subject: [PATCH 014/124] Display user name in application header --- matrixgw_backend/src/main.rs | 1 + matrixgw_frontend/src/api/AuthApi.ts | 6 +- .../dashboard/BaseAuthenticatedPage.tsx | 132 ++++++++++++------ .../src/widgets/dashboard/DashboardHeader.tsx | 44 +++++- .../src/widgets/dashboard/ThemeSwitcher.tsx | 32 ++--- 5 files changed, 147 insertions(+), 68 deletions(-) diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index a3fd905..a7073fe 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -17,6 +17,7 @@ async fn main() -> std::io::Result<()> { let secret_key = Key::from(AppConfig::get().secret().as_bytes()); + log::info!("Connect to Redis session store..."); let redis_store = RedisSessionStore::new(AppConfig::get().redis_connection_string()) .await .expect("Failed to connect to Redis!"); diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts index eb4b105..d3f60eb 100644 --- a/matrixgw_frontend/src/api/AuthApi.ts +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -1,6 +1,6 @@ import { APIClient } from "./ApiClient"; -export interface AuthInfo { +export interface UserInfo { id: number; time_create: number; time_update: number; @@ -58,9 +58,9 @@ export class AuthApi { } /** - * Get auth information + * Get user information */ - static async GetAuthInfo(): Promise { + static async GetUserInfo(): Promise { return ( await APIClient.exec({ uri: "/auth/info", diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index 60ae762..47e8be3 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -3,13 +3,39 @@ import { useTheme } from "@mui/material/styles"; import Toolbar from "@mui/material/Toolbar"; import useMediaQuery from "@mui/material/useMediaQuery"; import * as React from "react"; -import { Outlet } from "react-router"; +import { Outlet, useNavigate } from "react-router"; import DashboardHeader from "./DashboardHeader"; import DashboardSidebar from "./DashboardSidebar"; +import { AuthApi, type UserInfo } from "../../api/AuthApi"; +import { AsyncWidget } from "../AsyncWidget"; +import { Button } from "@mui/material"; +import { useAuth } from "../../App"; + +interface UserInfoContext { + info: UserInfo; + reloadUserInfo: () => void; + signOut: () => void; +} + +const UserInfoContextK = React.createContext(null); export default function BaseAuthenticatedPage(): React.ReactElement { const theme = useTheme(); + const [userInfo, setuserInfo] = React.useState(null); + const loadUserInfo = async () => { + setuserInfo(await AuthApi.GetUserInfo()); + }; + + const auth = useAuth(); + const navigate = useNavigate(); + + const signOut = () => { + AuthApi.SignOut(); + navigate("/"); + auth.setSignedIn(false); + }; + const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] = React.useState(false); const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] = @@ -46,47 +72,71 @@ export default function BaseAuthenticatedPage(): React.ReactElement { const layoutRef = React.useRef(null); return ( - - - - - - ( + <> + + + )} + build={() => ( + - - - - + + + + + + + + + + + + )} + /> ); } + +export function userUserInfo(): UserInfoContext { + return React.use(UserInfoContextK)!; +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx index 44444bc..0f76b97 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx @@ -1,7 +1,9 @@ import { mdiMessageTextFast } from "@mdi/js"; import Icon from "@mdi/react"; +import LogoutIcon from "@mui/icons-material/Logout"; import MenuIcon from "@mui/icons-material/Menu"; import MenuOpenIcon from "@mui/icons-material/MenuOpen"; +import { Avatar } from "@mui/material"; import MuiAppBar from "@mui/material/AppBar"; import Box from "@mui/material/Box"; import IconButton from "@mui/material/IconButton"; @@ -12,6 +14,7 @@ import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import * as React from "react"; import { RouterLink } from "../RouterLink"; +import { userUserInfo as useUserInfo } from "./BaseAuthenticatedPage"; import ThemeSwitcher from "./ThemeSwitcher"; const AppBar = styled(MuiAppBar)(({ theme }) => ({ @@ -42,6 +45,8 @@ export default function DashboardHeader({ menuOpen, onToggleMenu, }: DashboardHeaderProps) { + const user = useUserInfo(); + const handleMenuOpen = React.useCallback(() => { onToggleMenu(!menuOpen); }, [menuOpen, onToggleMenu]); @@ -101,6 +106,7 @@ export default function DashboardHeader({ ml: 1, whiteSpace: "nowrap", lineHeight: 1, + display: { xs: "none", sm: "block" }, }} > MatrixGW @@ -108,17 +114,41 @@ export default function DashboardHeader({ + + {/* User avatar */} - - - + + + + {user.info.name} + + + {user.info.email} + + + + + + + + - Hi TODO USER SIGN OUT diff --git a/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx b/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx index 0fd38c6..7c22dc3 100644 --- a/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx @@ -34,24 +34,22 @@ export default function ThemeSwitcher() { } mode`} onClick={toggleMode} > - - - - + }, + }} + /> +
    From 3dab9f41d20bf74fc86281116a29dca555b3521c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 5 Nov 2025 16:30:06 +0100 Subject: [PATCH 015/124] Start Matrix client authentication --- README.md | 4 +- matrixgw_backend/Cargo.lock | 2650 ++++++++++++++++- matrixgw_backend/Cargo.toml | 5 +- matrixgw_backend/src/app_config.rs | 50 + .../src/controllers/matrix_link_controller.rs | 14 + matrixgw_backend/src/controllers/mod.rs | 1 + .../src/extractors/matrix_client_extractor.rs | 37 + matrixgw_backend/src/extractors/mod.rs | 1 + matrixgw_backend/src/lib.rs | 1 + matrixgw_backend/src/main.rs | 28 +- .../src/matrix_connection/matrix_client.rs | 115 + .../src/matrix_connection/matrix_manager.rs | 62 + matrixgw_backend/src/matrix_connection/mod.rs | 2 + 13 files changed, 2960 insertions(+), 10 deletions(-) create mode 100644 matrixgw_backend/src/controllers/matrix_link_controller.rs create mode 100644 matrixgw_backend/src/extractors/matrix_client_extractor.rs create mode 100644 matrixgw_backend/src/matrix_connection/matrix_client.rs create mode 100644 matrixgw_backend/src/matrix_connection/matrix_manager.rs create mode 100644 matrixgw_backend/src/matrix_connection/mod.rs diff --git a/README.md b/README.md index 0c7620e..25667cd 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ docker run --rm -it docker.io/pierre42100/matrix_gateway --help ### Dependencies ``` +sudo apt install -y libsqlite3-dev + cd matrixgw_backend mkdir -p storage/maspostgres storage/synapse docker compose up @@ -53,4 +55,4 @@ cargo fmt && cargo clippy && cargo run -- cd matrixgw_frontend npm install npm run dev -``` \ No newline at end of file +``` diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index f857cd7..ebe0055 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "accessory" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87537f9ae7cfa78d5b8ebd1a1db25959f5e737126be4d8eb44a5452fc4b63cde" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "actix-codec" version = "0.5.2" @@ -294,6 +306,21 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.21" @@ -350,12 +377,38 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" + +[[package]] +name = "aquamarine" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" +dependencies = [ + "include_dir", + "itertools 0.10.5", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "arc-swap" version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "archery" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" + [[package]] name = "arrayref" version = "0.3.9" @@ -367,6 +420,15 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] + +[[package]] +name = "as_variant" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a" [[package]] name = "ascii_utils" @@ -374,6 +436,70 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" +[[package]] +name = "assign" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f093eed78becd229346bf859eec0aa4dd7ddde0757287b2b4107a1f09c80002" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -393,6 +519,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ "fastrand", + "gloo-timers", + "tokio", ] [[package]] @@ -436,6 +564,24 @@ name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitmaps" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6" + +[[package]] +name = "bitpacking" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" +dependencies = [ + "crunchy", +] [[package]] name = "blake2b_simd" @@ -448,6 +594,19 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -457,6 +616,63 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bon" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97493a391b4b18ee918675fb8663e53646fd09321c58b46afa04e8ce2499c869" +dependencies = [ + "bon-macros 2.3.0", + "rustversion", +] + +[[package]] +name = "bon" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb9aaf9329dff6ceb65c689ca3db33dbf15f324909c60e4e5eef5701ce31b1" +dependencies = [ + "bon-macros 3.8.1", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2af3eac944c12cdf4423eab70d310da0a8e5851a18ffb192c0a5e3f7ae1663" +dependencies = [ + "darling 0.20.11", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bon-macros" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e9d642a7e3a318e37c2c9427b5a6a48aa1ad55dcd986f3034ab2239045a645" +dependencies = [ + "darling 0.21.3", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "brotli" version = "8.0.2" @@ -478,18 +694,39 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "bytesize" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f" + [[package]] name = "bytestring" version = "1.5.0" @@ -499,6 +736,15 @@ dependencies = [ "bytes", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.44" @@ -511,12 +757,56 @@ dependencies = [ "shlex", ] +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + [[package]] name = "cipher" version = "0.4.4" @@ -525,6 +815,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -598,12 +889,47 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "compression-codecs" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -662,6 +988,46 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -700,6 +1066,187 @@ dependencies = [ "cipher", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "date_header" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c03c416ed1a30fbb027ef484ba6ab6f80e1eada675e1a2b92fd673c045a1f1d" + +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "deadpool-sqlite" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8510000b26f632483a35120c2ce280c29e1e14c2dcb27b5055dbdac276f63f58" +dependencies = [ + "deadpool", + "deadpool-sync", + "rusqlite", +] + +[[package]] +name = "deadpool-sync" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524bc3df0d57e98ecd022e21ba31166c2625e7d3e5bcc4510efaeeab4abcab04" +dependencies = [ + "deadpool-runtime", +] + +[[package]] +name = "decancer" +version = "3.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9244323129647178bf41ac861a2cdb9d9c81b9b09d3d0d1de9cd302b33b8a1d" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "der" version = "0.7.10" @@ -718,6 +1265,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", + "serde_core", ] [[package]] @@ -764,6 +1312,12 @@ dependencies = [ "syn", ] +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "ecdsa" version = "0.16.9" @@ -778,6 +1332,17 @@ dependencies = [ "spki", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + [[package]] name = "ed25519-compact" version = "2.1.1" @@ -788,6 +1353,27 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -857,6 +1443,77 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "eyeball" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93bd0ebf93d61d6332d3c09a96e97975968a44e19a64c947bde06e6baff383f" +dependencies = [ + "futures-core", + "readlock", + "readlock-tokio", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "eyeball-im" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e8e9d31591be508826b875d8fe6056aebcaec3281ac0e45434ff303686c566" +dependencies = [ + "futures-core", + "imbl", + "tokio", + "tracing", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fancy_constructor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07b19d0e43eae2bfbafe4931b5e79c73fb1a849ca15cd41a761a7b8587f9a1a2" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "fast_chemail" version = "0.9.6" @@ -866,6 +1523,12 @@ dependencies = [ "ascii_utils", ] +[[package]] +name = "fastdivide" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" + [[package]] name = "fastrand" version = "2.3.0" @@ -882,6 +1545,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.4" @@ -934,6 +1603,41 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -941,6 +1645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -949,6 +1654,23 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + [[package]] name = "futures-macro" version = "0.3.31" @@ -978,10 +1700,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1033,6 +1758,31 @@ dependencies = [ "polyval", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "group" version = "0.13.0" @@ -1044,6 +1794,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "growable-bloom-filter" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d174ccb4ba660d431329e7f0797870d0a4281e36353ec4b4a3c5eab6c2cfb6f1" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "xxhash-rust", +] + [[package]] name = "h2" version = "0.3.27" @@ -1082,18 +1844,74 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http 1.3.1", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.3.1", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1142,6 +1960,24 @@ dependencies = [ "digest", ] +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "0.2.12" @@ -1164,6 +2000,15 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-auth" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" +dependencies = [ + "memchr", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1279,6 +2124,39 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "hyperloglogplus" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3" +dependencies = [ + "serde", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -1360,6 +2238,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.1.0" @@ -1381,12 +2265,72 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "imbl" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4308a675e4cfc1920f36a8f4d8fb62d5533b7da106844bd1ec51c6f1fa94a0c" +dependencies = [ + "archery", + "bitmaps", + "imbl-sized-chunks", + "rand_core 0.9.3", + "rand_xoshiro", + "serde", + "version_check", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4241005618a62f8d57b2febd02510fb96e0137304728543dfc5fd6f052c22d" +dependencies = [ + "bitmaps", +] + [[package]] name = "impl-more" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexed_db_futures" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43315957678a70eb21fb0d2384fe86dde0d6c859a01e24ce127eb65a0143d28c" +dependencies = [ + "accessory", + "cfg-if", + "delegate-display", + "fancy_constructor", + "js-sys", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "indexmap" version = "2.12.0" @@ -1394,7 +2338,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", + "serde", + "serde_core", ] [[package]] @@ -1403,6 +2349,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding", "generic-array", ] @@ -1431,6 +2378,24 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1481,6 +2446,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "js_int" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d937f95470b270ce8b8950207715d71aa8e153c0d44c6684d59397ed4949160a" +dependencies = [ + "serde", +] + +[[package]] +name = "js_option" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68421373957a1593a767013698dbf206e2b221eefe97a44d98d18672ff38423c" +dependencies = [ + "serde", +] + [[package]] name = "jwt-simple" version = "0.12.13" @@ -1503,7 +2486,7 @@ dependencies = [ "serde", "serde_json", "superboring", - "thiserror", + "thiserror 2.0.17", "zeroize", ] @@ -1521,6 +2504,26 @@ dependencies = [ "signature", ] +[[package]] +name = "konst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4381b9b00c55f251f2ebe9473aef7c117e96828def1a7cb3bd3f0f903c6894e9" +dependencies = [ + "const_panic", + "konst_kernel", + "typewit", +] + +[[package]] +name = "konst_kernel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4b1eb7788f3824c629b1116a7a9060d6e898c358ebff59070093d51103dcc3c" +dependencies = [ + "typewit", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1536,6 +2539,12 @@ dependencies = [ "spin", ] +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + [[package]] name = "libc" version = "0.2.177" @@ -1548,6 +2557,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "light-openid" version = "1.0.4" @@ -1606,6 +2625,74 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "mailchecker" version = "6.0.19" @@ -1616,6 +2703,298 @@ dependencies = [ "once_cell", ] +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "matrix-pickle" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c34e6db65145740459f2ca56623b40cd4e6000ffae2a7d91515fa82aa935dbf" +dependencies = [ + "matrix-pickle-derive", + "thiserror 2.0.17", +] + +[[package]] +name = "matrix-pickle-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a962fc9981f823f6555416dcb2ae9ae67ca412d767ee21ecab5150113ee6285b" +dependencies = [ + "proc-macro-crate", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "matrix-sdk" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdfa71339f867dcada2e7f1130f858fd8892088b1a4c123dd50a99ed2399ab22" +dependencies = [ + "anymap2", + "aquamarine", + "as_variant", + "async-channel", + "async-stream", + "async-trait", + "backon", + "bytes", + "bytesize", + "cfg-if", + "event-listener", + "eyeball", + "eyeball-im", + "futures-core", + "futures-util", + "gloo-timers", + "http 1.3.1", + "imbl", + "indexmap", + "itertools 0.14.0", + "js_int", + "language-tags", + "matrix-sdk-base", + "matrix-sdk-common", + "matrix-sdk-indexeddb", + "matrix-sdk-search", + "matrix-sdk-sqlite", + "mime", + "mime2ext", + "oauth2", + "once_cell", + "percent-encoding", + "pin-project-lite", + "reqwest", + "ruma", + "serde", + "serde_html_form", + "serde_json", + "sha2", + "tempfile", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "urlencoding", + "vodozemac", + "zeroize", +] + +[[package]] +name = "matrix-sdk-base" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14659a7e902ea8a821ec217f36b168fb4c79020d91b912175ac188b6c364225" +dependencies = [ + "as_variant", + "async-trait", + "bitflags", + "decancer", + "eyeball", + "eyeball-im", + "futures-util", + "growable-bloom-filter", + "matrix-sdk-common", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "once_cell", + "regex", + "ruma", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "unicode-normalization", +] + +[[package]] +name = "matrix-sdk-common" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb33a986495135e217f28cfe0918bf7d01a9800e42f4ef88afbd48e23b8cc53" +dependencies = [ + "eyeball-im", + "futures-core", + "futures-executor", + "futures-util", + "gloo-timers", + "imbl", + "ruma", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", + "tracing-subscriber", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "matrix-sdk-crypto" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61bf6c3195de301c98339283413a4e9d9d63c4f214ef8955147643caab161256" +dependencies = [ + "aes", + "aquamarine", + "as_variant", + "async-trait", + "bs58", + "byteorder", + "cfg-if", + "ctr", + "eyeball", + "futures-core", + "futures-util", + "hkdf", + "hmac", + "itertools 0.14.0", + "js_option", + "matrix-sdk-common", + "pbkdf2", + "rand 0.8.5", + "rmp-serde", + "ruma", + "serde", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.17", + "time", + "tokio", + "tokio-stream", + "tracing", + "ulid", + "url", + "vodozemac", + "zeroize", +] + +[[package]] +name = "matrix-sdk-indexeddb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2752015e69b6b56a8df72e52f91a834771259f755eb1c65232b77348ffb16b" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "getrandom 0.2.16", + "gloo-utils", + "hkdf", + "indexed_db_futures", + "js-sys", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "rmp-serde", + "ruma", + "serde", + "serde-wasm-bindgen", + "serde_json", + "sha2", + "thiserror 2.0.17", + "tokio", + "tracing", + "wasm-bindgen", + "web-sys", + "zeroize", +] + +[[package]] +name = "matrix-sdk-search" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7474bd9045b6ce28fbe5c903a1cef2ba43264e51386c1d07f61699a178f3fec0" +dependencies = [ + "ruma", + "tantivy", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "matrix-sdk-sqlite" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662a128004b553196365d88ea46917047d7350a24dfe7ce829e7b78636673604" +dependencies = [ + "as_variant", + "async-trait", + "deadpool-sqlite", + "itertools 0.14.0", + "matrix-sdk-base", + "matrix-sdk-crypto", + "matrix-sdk-store-encryption", + "num_cpus", + "rmp-serde", + "ruma", + "rusqlite", + "serde", + "serde_json", + "serde_path_to_error", + "thiserror 2.0.17", + "tokio", + "tracing", + "vodozemac", +] + +[[package]] +name = "matrix-sdk-store-encryption" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e0aac550e685306fbbd57faae8f98af9812f19bbf0f83da7559d67cf4789679" +dependencies = [ + "base64 0.22.1", + "blake3", + "chacha20poly1305", + "hmac", + "pbkdf2", + "rand 0.8.5", + "rmp-serde", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.17", + "zeroize", +] + [[package]] name = "matrixgw_backend" version = "0.1.0" @@ -1637,27 +3016,60 @@ dependencies = [ "light-openid", "log", "mailchecker", + "matrix-sdk", + "ractor", "rand 0.9.2", "serde", "sha2", - "thiserror", + "thiserror 2.0.17", "tokio", + "url", "urlencoding", "uuid", ] +[[package]] +name = "measure_time" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" +dependencies = [ + "log", +] + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime2ext" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1680,6 +3092,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "murmurhash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" + [[package]] name = "native-tls" version = "0.2.14" @@ -1697,6 +3115,31 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -1759,6 +3202,36 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64 0.22.1", + "chrono", + "getrandom 0.2.16", + "http 1.3.1", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1771,6 +3244,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oneshot" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -1821,6 +3300,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ownedbytes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "p256" version = "0.13.2" @@ -1845,6 +3333,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1868,6 +3362,22 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1883,6 +3393,44 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1922,6 +3470,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -1973,6 +3532,22 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -1982,6 +3557,36 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -1991,6 +3596,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.41" @@ -2006,6 +3634,26 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "ractor" +version = "0.15.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9500e0be6f12a0539cb1154d654ef2e888bf8529164e54aff4a097baad5bb001" +dependencies = [ + "bon 2.3.0", + "dashmap", + "futures", + "js-sys", + "once_cell", + "strum", + "tokio", + "tokio_with_wasm", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + [[package]] name = "rand" version = "0.8.5" @@ -2065,6 +3713,60 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "readlock" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "188bbae3aa4739bd264e9204da5919b2c91dd87dcce5049cf04bdf6aa17c5012" + +[[package]] +name = "readlock-tokio" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b1800712c0d75de4b0bda5483d46eaf8df757b81df5ca2bde53d5ac2e2c5b2" +dependencies = [ + "tokio", +] + [[package]] name = "redis" version = "0.32.7" @@ -2139,10 +3841,12 @@ version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ + "async-compression", "base64 0.22.1", "bytes", "encoding_rs", "futures-core", + "futures-util", "h2 0.4.12", "http 1.3.1", "http-body", @@ -2164,12 +3868,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] @@ -2197,6 +3903,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rsa" version = "0.9.8" @@ -2218,6 +3946,221 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ruma" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b698b728bc3747f564a9115c83b4f2e229b52377f6a1cca2e6add9cf4a13be" +dependencies = [ + "assign", + "js_int", + "js_option", + "ruma-client-api", + "ruma-common", + "ruma-events", + "ruma-federation-api", + "ruma-html", + "ruma-signatures", + "web-time", +] + +[[package]] +name = "ruma-client-api" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54e56c591f9ad686defb0bacbebba5c8882eb0c9f8734f6a080345b4e3dd941" +dependencies = [ + "as_variant", + "assign", + "bytes", + "date_header", + "http 1.3.1", + "js_int", + "js_option", + "maplit", + "ruma-common", + "ruma-events", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.17", + "url", + "web-time", +] + +[[package]] +name = "ruma-common" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac7f59b9f7639667d0d6ae3ae242c8912e9ed061cea1fbaf72710a402e83b53e" +dependencies = [ + "as_variant", + "base64 0.22.1", + "bytes", + "form_urlencoded", + "getrandom 0.2.16", + "http 1.3.1", + "indexmap", + "js-sys", + "js_int", + "konst", + "percent-encoding", + "rand 0.8.5", + "regex", + "ruma-identifiers-validation", + "ruma-macros", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.17", + "time", + "tracing", + "url", + "uuid", + "web-time", + "wildmatch", + "zeroize", +] + +[[package]] +name = "ruma-events" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34fa815769ed4fe1ef5b50aa0ba6f350317c13b5a9f1e008b014f4a3ddf14204" +dependencies = [ + "as_variant", + "indexmap", + "js_int", + "js_option", + "percent-encoding", + "regex", + "ruma-common", + "ruma-identifiers-validation", + "ruma-macros", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", + "url", + "web-time", + "wildmatch", + "zeroize", +] + +[[package]] +name = "ruma-federation-api" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecbc887ba1292e48e6363b29e0dec4571b52d2b5102ebf60068105efadaa6e0a" +dependencies = [ + "headers", + "http 1.3.1", + "http-auth", + "httparse", + "js_int", + "memchr", + "mime", + "ruma-common", + "ruma-events", + "serde", + "serde_json", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "ruma-html" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6124d74847ea788601477c89a44485894432a806824cae93885c5825a8ae9dbc" +dependencies = [ + "as_variant", + "html5ever", + "tracing", + "wildmatch", +] + +[[package]] +name = "ruma-identifiers-validation" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a7b93ac1e571c585f8fa5cef09c07bb8a15529775fd56b9a3eac4f9233dff2" +dependencies = [ + "js_int", + "thiserror 2.0.17", +] + +[[package]] +name = "ruma-macros" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9911c7188517f28505d2d513339511d00e0f50cec5c2dde820cd0ec7e6a833" +dependencies = [ + "cfg-if", + "proc-macro-crate", + "proc-macro2", + "quote", + "ruma-identifiers-validation", + "serde", + "syn", + "toml", +] + +[[package]] +name = "ruma-signatures" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47cd146d56ae6e7a4a8d912a30dfe57c70e5bf18806fdf617527d4d4f2dd2a4" +dependencies = [ + "base64 0.22.1", + "ed25519-dalek", + "pkcs8", + "rand 0.8.5", + "ruma-common", + "serde_json", + "sha2", + "thiserror 2.0.17", +] + +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[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 = "1.1.2" @@ -2328,6 +4271,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -2338,6 +4287,27 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2358,6 +4328,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde_core", +] + [[package]] name = "serde_json" version = "1.0.145" @@ -2371,6 +4354,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2405,6 +4408,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2436,6 +4448,21 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" +dependencies = [ + "serde", +] + [[package]] name = "slab" version = "0.4.11" @@ -2490,12 +4517,59 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2567,6 +4641,152 @@ dependencies = [ "libc", ] +[[package]] +name = "tantivy" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502915c7381c5cb2d2781503962610cb880ad8f1a0ca95df1bae645d5ebf2545" +dependencies = [ + "aho-corasick", + "arc-swap", + "base64 0.22.1", + "bitpacking", + "bon 3.8.1", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fastdivide", + "fnv", + "fs4", + "htmlescape", + "hyperloglogplus", + "itertools 0.14.0", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror 2.0.17", + "time", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b04eed5108d8283607da6710fe17a7663523440eaf7ea5a1a440d19a1448b6" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b628488ae936c83e92b5c4056833054ca56f76c0e616aee8339e24ac89119cd" +dependencies = [ + "downcast-rs", + "fastdivide", + "itertools 0.14.0", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f880aa7cab0c063a47b62596d10991cdd0b6e0e0575d9c5eeb298b307a25de55" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" +dependencies = [ + "byteorder", + "regex-syntax", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768fccdc84d60d86235d42d7e4c33acf43c418258ff5952abf07bd7837fcd26b" +dependencies = [ + "nom", + "serde", + "serde_json", +] + +[[package]] +name = "tantivy-sstable" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8292095d1a8a2c2b36380ec455f910ab52dde516af36321af332c93f20ab7d5" +dependencies = [ + "futures-util", + "itertools 0.14.0", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-fst", + "zstd", +] + +[[package]] +name = "tantivy-stacker" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d38a379411169f0b3002c9cba61cdfe315f757e9d4f239c00c282497a0749d" +dependencies = [ + "murmurhash32", + "rand_distr", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23024f6aeb25ceb1a0e27740c84bdb0fae52626737b7e9a9de6ad5aa25c7b038" +dependencies = [ + "serde", +] + [[package]] name = "tempfile" version = "3.23.0" @@ -2580,13 +4800,44 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2600,6 +4851,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.44" @@ -2641,6 +4901,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" @@ -2655,6 +4930,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.6.1", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -2689,6 +4965,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -2702,6 +4990,94 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio_with_wasm" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dfba9b946459940fb564dcf576631074cdfb0bfe4c962acd4c31f0dca7897e6" +dependencies = [ + "js-sys", + "tokio", + "tokio_with_wasm_proc", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "tokio_with_wasm_proc" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e04c1865c281139e5ccf633cb9f76ffdaabeebfe53b703984cf82878e2aabb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime 0.7.3", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.2" @@ -2777,6 +5153,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -2791,12 +5193,46 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "typewit" +version = "1.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" +dependencies = [ + "typewit_proc_macros", +] + +[[package]] +name = "typewit_proc_macros" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.2", + "web-time", +] + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2837,6 +5273,18 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2861,6 +5309,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -2873,6 +5327,36 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vodozemac" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c022a277687e4e8685d72b95a7ca3ccfec907daa946678e715f8badaa650883d" +dependencies = [ + "aes", + "arrayvec", + "base64 0.22.1", + "base64ct", + "cbc", + "chacha20poly1305", + "curve25519-dalek", + "ed25519-dalek", + "getrandom 0.2.16", + "hkdf", + "hmac", + "matrix-pickle", + "prost", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_json", + "sha2", + "subtle", + "thiserror 2.0.17", + "x25519-dalek", + "zeroize", +] + [[package]] name = "want" version = "0.3.1" @@ -2964,6 +5448,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.82" @@ -2974,6 +5471,79 @@ 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 = "wildmatch" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b7d07a236abaef6607536ccfaf19b396dbe3f5110ddb73d39f4562902ed382" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.3" @@ -2993,8 +5563,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -3006,6 +5576,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -3015,6 +5594,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -3024,6 +5612,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -3171,6 +5768,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" @@ -3183,6 +5789,24 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "xxhash-rust" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" + [[package]] name = "yoke" version = "0.8.1" @@ -3252,6 +5876,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index ffe9d1e..94f3b2b 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -27,4 +27,7 @@ uuid = { version = "1.18.1", features = ["v4", "serde"] } ipnet = { version = "2.11.0", features = ["serde"] } rand = "0.9.2" hex = "0.4.3" -mailchecker = "6.0.19" \ No newline at end of file +mailchecker = "6.0.19" +matrix-sdk = "0.14.0" +url = "2.5.7" +ractor = "0.15.9" \ No newline at end of file diff --git a/matrixgw_backend/src/app_config.rs b/matrixgw_backend/src/app_config.rs index ed122d8..32fa673 100644 --- a/matrixgw_backend/src/app_config.rs +++ b/matrixgw_backend/src/app_config.rs @@ -1,7 +1,11 @@ use crate::users::{APITokenID, UserEmail}; use crate::utils::crypt_utils::sha256str; use clap::Parser; +use matrix_sdk::authentication::oauth::registration::{ + ApplicationType, ClientMetadata, Localized, OAuthGrantType, +}; use std::path::{Path, PathBuf}; +use url::Url; /// Matrix gateway backend API #[derive(Parser, Debug, Clone)] @@ -76,6 +80,10 @@ pub struct AppConfig { #[arg(long, env, default_value = "APP_ORIGIN/oidc_cb")] oidc_redirect_url: String, + /// Matrix oauth redirect URL + #[arg(long, env, default_value = "APP_ORIGIN/matrix_auth_cb")] + matrix_oauth_redirect_url: String, + /// Application storage path #[arg(long, env, default_value = "app_storage")] storage_path: String, @@ -146,6 +154,38 @@ impl AppConfig { } } + /// Matrix OAuth redirect URL + pub fn matrix_oauth_redirect_url(&self) -> String { + self.matrix_oauth_redirect_url + .replace("APP_ORIGIN", &self.website_origin) + } + + /// Get Matrix client metadata information + pub fn matrix_client_metadata(&self) -> ClientMetadata { + let client_uri = Localized::new( + Url::parse(&self.website_origin).expect("Invalid website origin!"), + [], + ); + ClientMetadata { + application_type: ApplicationType::Native, + grant_types: vec![OAuthGrantType::AuthorizationCode { + redirect_uris: vec![ + Url::parse(&self.matrix_oauth_redirect_url()) + .expect("Failed to parse matrix auth redirect URI!"), + ], + }], + client_name: Some(Localized::new("MatrixGW".to_string(), [])), + logo_uri: Some(Localized::new( + Url::parse(&format!("{}/favicon.png", self.website_origin)) + .expect("Invalid website origin!"), + [], + )), + policy_uri: Some(client_uri.clone()), + tos_uri: Some(client_uri.clone()), + client_uri, + } + } + /// Get storage path pub fn storage_path(&self) -> &Path { Path::new(self.storage_path.as_str()) @@ -170,6 +210,16 @@ impl AppConfig { pub fn user_api_token_metadata_file(&self, mail: &UserEmail, id: &APITokenID) -> PathBuf { self.user_api_token_directory(mail).join(id.0.to_string()) } + + /// Get user Matrix database path + pub fn user_matrix_db_path(&self, mail: &UserEmail) -> PathBuf { + self.user_directory(mail).join("matrix-db") + } + + /// Get user Matrix database passphrase path + pub fn user_matrix_passphrase_path(&self, mail: &UserEmail) -> PathBuf { + self.user_directory(mail).join("matrix-db-passphrase") + } } #[derive(Debug, Clone, serde::Serialize)] diff --git a/matrixgw_backend/src/controllers/matrix_link_controller.rs b/matrixgw_backend/src/controllers/matrix_link_controller.rs new file mode 100644 index 0000000..38fea49 --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix_link_controller.rs @@ -0,0 +1,14 @@ +use crate::controllers::HttpResult; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use actix_web::HttpResponse; + +#[derive(serde::Serialize)] +struct StartAuthResponse { + url: String, +} + +/// Start user authentication on Matrix server +pub async fn start_auth(client: MatrixClientExtractor) -> HttpResult { + let url = client.client.initiate_login().await?.to_string(); + Ok(HttpResponse::Ok().json(StartAuthResponse { url })) +} diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 25104bf..8a581e9 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -3,6 +3,7 @@ use actix_web::{HttpResponse, ResponseError}; use std::error::Error; pub mod auth_controller; +pub mod matrix_link_controller; pub mod server_controller; #[derive(thiserror::Error, Debug)] diff --git a/matrixgw_backend/src/extractors/matrix_client_extractor.rs b/matrixgw_backend/src/extractors/matrix_client_extractor.rs new file mode 100644 index 0000000..fbe6d3c --- /dev/null +++ b/matrixgw_backend/src/extractors/matrix_client_extractor.rs @@ -0,0 +1,37 @@ +use crate::extractors::auth_extractor::AuthExtractor; +use crate::matrix_connection::matrix_client::MatrixClient; +use crate::matrix_connection::matrix_manager::MatrixManagerMsg; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest, web}; +use ractor::ActorRef; + +pub struct MatrixClientExtractor { + pub auth: AuthExtractor, + pub client: MatrixClient, +} + +impl FromRequest for MatrixClientExtractor { + type Error = actix_web::Error; + type Future = futures_util::future::LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + let req = req.clone(); + let mut payload = payload.take(); + Box::pin(async move { + let auth = AuthExtractor::from_request(&req, &mut payload).await?; + + let matrix_manager_actor = + web::Data::>::from_request(&req, &mut Payload::None) + .await?; + let client = ractor::call!( + matrix_manager_actor, + MatrixManagerMsg::GetClient, + auth.user.email.clone() + ) + .expect("Failed to query manager actor!") + .expect("Failed to get client!"); + + Ok(Self { auth, client }) + }) + } +} diff --git a/matrixgw_backend/src/extractors/mod.rs b/matrixgw_backend/src/extractors/mod.rs index 4e4f77f..546e4d2 100644 --- a/matrixgw_backend/src/extractors/mod.rs +++ b/matrixgw_backend/src/extractors/mod.rs @@ -1,2 +1,3 @@ pub mod auth_extractor; +pub mod matrix_client_extractor; pub mod session_extractor; diff --git a/matrixgw_backend/src/lib.rs b/matrixgw_backend/src/lib.rs index 82c2228..61b9e52 100644 --- a/matrixgw_backend/src/lib.rs +++ b/matrixgw_backend/src/lib.rs @@ -2,5 +2,6 @@ pub mod app_config; pub mod constants; pub mod controllers; pub mod extractors; +pub mod matrix_connection; pub mod users; pub mod utils; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index a7073fe..2571cdc 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -8,8 +8,10 @@ use actix_web::middleware::Logger; use actix_web::{App, HttpServer, web}; use matrixgw_backend::app_config::AppConfig; use matrixgw_backend::constants; -use matrixgw_backend::controllers::{auth_controller, server_controller}; +use matrixgw_backend::controllers::{auth_controller, matrix_link_controller, server_controller}; +use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor; use matrixgw_backend::users::User; +use ractor::Actor; #[tokio::main] async fn main() -> std::io::Result<()> { @@ -29,12 +31,22 @@ async fn main() -> std::io::Result<()> { .expect("Failed to create auto-login account!"); } + // Create matrix clients manager actor + let (manager_actor, manager_actor_handle) = Actor::spawn( + Some("matrix-clients-manager".to_string()), + MatrixManagerActor, + (), + ) + .await + .expect("Failed to start Matrix manager actor!"); + log::info!( "Starting to listen on {} for {}", AppConfig::get().listen_address, AppConfig::get().website_origin ); + let manager_actor_clone = manager_actor.clone(); HttpServer::new(move || { let session_mw = SessionMiddleware::builder(redis_store.clone(), secret_key.clone()) .cookie_name("matrixgw-session".to_string()) @@ -53,6 +65,7 @@ async fn main() -> std::io::Result<()> { .wrap(Logger::default()) .wrap(session_mw) .wrap(cors) + .app_data(web::Data::new(manager_actor_clone.clone())) .app_data(web::Data::new(RemoteIPConfig { proxy: AppConfig::get().proxy_ip.clone(), })) @@ -76,9 +89,20 @@ async fn main() -> std::io::Result<()> { "/api/auth/sign_out", web::get().to(auth_controller::sign_out), ) + // Matrix link controller + .route( + "/api/matrix_link/start_auth", + web::post().to(matrix_link_controller::start_auth), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? .run() - .await + .await?; + + // Terminate manager actor + manager_actor.stop(None); + manager_actor_handle.await?; + + Ok(()) } diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs new file mode 100644 index 0000000..f6dcc86 --- /dev/null +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -0,0 +1,115 @@ +use crate::app_config::AppConfig; +use crate::users::UserEmail; +use crate::utils::rand_utils::rand_string; +use matrix_sdk::authentication::oauth::OAuthError; +use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError; +use matrix_sdk::ruma::serde::Raw; +use matrix_sdk::{Client, ClientBuildError}; +use url::Url; + +/// Matrix Gateway session errors +#[derive(thiserror::Error, Debug)] +enum MatrixClientError { + #[error("Failed to create Matrix database storage directory! {0}")] + CreateMatrixDbDir(std::io::Error), + #[error("Failed to create database passphrase! {0}")] + CreateDbPassphrase(std::io::Error), + #[error("Failed to read database passphrase! {0}")] + ReadDbPassphrase(std::io::Error), + #[error("Failed to build Matrix client! {0}")] + BuildMatrixClient(ClientBuildError), + #[error("Failed to clear Matrix database storage directory! {0}")] + ClearMatrixDbDir(std::io::Error), + #[error("Failed to remove database passphrase! {0}")] + ClearDbPassphrase(std::io::Error), + #[error("Failed to fetch server metadata! {0}")] + FetchServerMetadata(OAuthDiscoveryError), + #[error("Failed to parse auth redirect URL! {0}")] + ParseAuthRedirectURL(url::ParseError), + #[error("Failed to build auth request! {0}")] + BuildAuthRequest(OAuthError), +} + +#[derive(Clone)] +pub struct MatrixClient { + pub email: UserEmail, + pub client: Client, +} + +impl MatrixClient { + /// Start to build Matrix client to initiate user authentication + pub async fn build_client(email: &UserEmail) -> anyhow::Result { + let db_path = AppConfig::get().user_matrix_db_path(email); + std::fs::create_dir_all(&db_path).map_err(MatrixClientError::CreateMatrixDbDir)?; + + // Generate or load passphrase + let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email); + if !passphrase_path.exists() { + std::fs::write(&passphrase_path, rand_string(32)) + .map_err(MatrixClientError::CreateDbPassphrase)?; + } + let passphrase = std::fs::read_to_string(passphrase_path) + .map_err(MatrixClientError::ReadDbPassphrase)?; + + let client = Client::builder() + .server_name_or_homeserver_url(&AppConfig::get().matrix_homeserver) + // Automatically refresh tokens if needed + .handle_refresh_tokens() + .sqlite_store(&db_path, Some(&passphrase)) + .build() + .await + .map_err(MatrixClientError::BuildMatrixClient)?; + + // Check metadata + let server_metadata = client + .oauth() + .server_metadata() + .await + .map_err(MatrixClientError::FetchServerMetadata)?; + log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer); + + // TODO : restore client if client already existed + + Ok(Self { + email: email.clone(), + client, + }) + } + + /// Destroy this Matrix client instance + pub fn destroy(&self) -> anyhow::Result<()> { + let db_path = AppConfig::get().user_matrix_db_path(&self.email); + if db_path.is_file() { + std::fs::remove_dir_all(&db_path).map_err(MatrixClientError::ClearMatrixDbDir)?; + } + + let passphrase_path = AppConfig::get().user_matrix_passphrase_path(&self.email); + if passphrase_path.is_file() { + std::fs::remove_file(passphrase_path).map_err(MatrixClientError::ClearDbPassphrase)?; + } + + todo!() + } + + /// Initiate oauth authentication + pub async fn initiate_login(&self) -> anyhow::Result { + let oauth = self.client.oauth(); + + let metadata = AppConfig::get().matrix_client_metadata(); + let client_metadata = Raw::new(&metadata).expect("Couldn't serialize client metadata"); + + let auth = oauth + .login( + Url::parse(&AppConfig::get().matrix_oauth_redirect_url()) + .map_err(MatrixClientError::ParseAuthRedirectURL)?, + None, + Some(client_metadata.into()), + None, + ) + .build() + .await + .map_err(MatrixClientError::BuildAuthRequest)?; + + Ok(auth.url) + } +} diff --git a/matrixgw_backend/src/matrix_connection/matrix_manager.rs b/matrixgw_backend/src/matrix_connection/matrix_manager.rs new file mode 100644 index 0000000..d6f180a --- /dev/null +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -0,0 +1,62 @@ +use crate::matrix_connection::matrix_client::MatrixClient; +use crate::users::UserEmail; +use ractor::{Actor, ActorProcessingErr, ActorRef, RpcReplyPort}; +use std::collections::HashMap; + +pub struct MatrixManagerState { + pub clients: HashMap, +} + +pub enum MatrixManagerMsg { + GetClient(UserEmail, RpcReplyPort>), +} + +pub struct MatrixManagerActor; + +impl Actor for MatrixManagerActor { + type Msg = MatrixManagerMsg; + type State = MatrixManagerState; + type Arguments = (); + + async fn pre_start( + &self, + _myself: ActorRef, + _args: Self::Arguments, + ) -> Result { + Ok(MatrixManagerState { + clients: HashMap::new(), + }) + } + + async fn handle( + &self, + _myself: ActorRef, + message: Self::Msg, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + match message { + // Get client information + MatrixManagerMsg::GetClient(email, port) => { + let res = port.send(match state.clients.get(&email) { + None => { + // Generate client if required + log::info!("Building new client for {:?}", &email); + match MatrixClient::build_client(&email).await { + Ok(c) => { + state.clients.insert(email.clone(), c.clone()); + Ok(c) + } + Err(e) => Err(e), + } + } + Some(c) => Ok(c.clone()), + }); + + if let Err(e) = res { + log::warn!("Failed to send client information: {e}") + } + } + } + Ok(()) + } +} diff --git a/matrixgw_backend/src/matrix_connection/mod.rs b/matrixgw_backend/src/matrix_connection/mod.rs new file mode 100644 index 0000000..c929f1b --- /dev/null +++ b/matrixgw_backend/src/matrix_connection/mod.rs @@ -0,0 +1,2 @@ +pub mod matrix_client; +pub mod matrix_manager; From 37fad9ff551f1a3d80f2a13fc992ae2bfe5bbd93 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 5 Nov 2025 18:27:41 +0100 Subject: [PATCH 016/124] Can start Matrix authentication from UI --- .../src/controllers/auth_controller.rs | 4 +- matrixgw_backend/src/users.rs | 16 +++ matrixgw_frontend/src/App.tsx | 2 + matrixgw_frontend/src/api/AuthApi.ts | 1 + matrixgw_frontend/src/api/MatrixLinkApi.ts | 15 +++ matrixgw_frontend/src/routes/HomeRoute.tsx | 7 ++ .../src/routes/MatrixLinkRoute.tsx | 98 +++++++++++++++++++ .../src/widgets/MatrixGWRouteContainer.tsx | 37 +++++++ .../src/widgets/NotLinkedAccountMessage.tsx | 14 +++ .../dashboard/BaseAuthenticatedPage.tsx | 3 +- .../src/widgets/dashboard/DashboardHeader.tsx | 2 +- .../widgets/dashboard/DashboardSidebar.tsx | 2 +- 12 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 matrixgw_frontend/src/api/MatrixLinkApi.ts create mode 100644 matrixgw_frontend/src/routes/MatrixLinkRoute.tsx create mode 100644 matrixgw_frontend/src/widgets/MatrixGWRouteContainer.tsx create mode 100644 matrixgw_frontend/src/widgets/NotLinkedAccountMessage.tsx diff --git a/matrixgw_backend/src/controllers/auth_controller.rs b/matrixgw_backend/src/controllers/auth_controller.rs index ab14ace..6cbab4c 100644 --- a/matrixgw_backend/src/controllers/auth_controller.rs +++ b/matrixgw_backend/src/controllers/auth_controller.rs @@ -2,7 +2,7 @@ use crate::app_config::AppConfig; use crate::controllers::{HttpFailure, HttpResult}; use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; use crate::extractors::session_extractor::MatrixGWSession; -use crate::users::{User, UserEmail}; +use crate::users::{ExtendedUserInfo, User, UserEmail}; use actix_remote_ip::RemoteIP; use actix_web::{HttpResponse, web}; use light_openid::primitives::OpenIDConfig; @@ -108,7 +108,7 @@ pub async fn finish_oidc( /// Get current user information pub async fn auth_info(auth: AuthExtractor) -> HttpResult { - Ok(HttpResponse::Ok().json(auth.user)) + Ok(HttpResponse::Ok().json(ExtendedUserInfo::from_user(auth.user).await?)) } /// Sign out user diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs index 2cb1e0a..aab18ed 100644 --- a/matrixgw_backend/src/users.rs +++ b/matrixgw_backend/src/users.rs @@ -166,3 +166,19 @@ impl APIToken { (self.last_used + self.max_inactivity) < time_secs() } } + +#[derive(serde::Serialize, Debug, Clone)] +pub struct ExtendedUserInfo { + #[serde(flatten)] + pub user: User, + pub matrix_user_id: Option, +} + +impl ExtendedUserInfo { + pub async fn from_user(user: User) -> anyhow::Result { + Ok(Self { + user, + matrix_user_id: None, // TODO + }) + } +} diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index 22acab2..9988388 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -10,6 +10,7 @@ import { ServerApi } from "./api/ServerApi"; import { LoginRoute } from "./routes/auth/LoginRoute"; import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; import { HomeRoute } from "./routes/HomeRoute"; +import { MatrixLinkRoute } from "./routes/MatrixLinkRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute"; import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage"; @@ -37,6 +38,7 @@ export function App(): React.ReactElement { signedIn || ServerApi.Config.auth_disabled ? ( }> } /> + } /> } /> ) : ( diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts index d3f60eb..c0fc286 100644 --- a/matrixgw_frontend/src/api/AuthApi.ts +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -6,6 +6,7 @@ export interface UserInfo { time_update: number; name: string; email: string; + matrix_user_id?: string; } const TokenStateKey = "auth-state"; diff --git a/matrixgw_frontend/src/api/MatrixLinkApi.ts b/matrixgw_frontend/src/api/MatrixLinkApi.ts new file mode 100644 index 0000000..6a05a3c --- /dev/null +++ b/matrixgw_frontend/src/api/MatrixLinkApi.ts @@ -0,0 +1,15 @@ +import { APIClient } from "./ApiClient"; + +export class MatrixLinkApi { + /** + * Start Matrix Account login + */ + static async StartAuth(): Promise<{ url: string }> { + return ( + await APIClient.exec({ + uri: "/matrix_link/start_auth", + method: "POST", + }) + ).data; + } +} diff --git a/matrixgw_frontend/src/routes/HomeRoute.tsx b/matrixgw_frontend/src/routes/HomeRoute.tsx index b93b24f..ee0ddf1 100644 --- a/matrixgw_frontend/src/routes/HomeRoute.tsx +++ b/matrixgw_frontend/src/routes/HomeRoute.tsx @@ -1,3 +1,10 @@ +import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; +import { NotLinkedAccountMessage } from "../widgets/NotLinkedAccountMessage"; + export function HomeRoute(): React.ReactElement { + const user = useUserInfo(); + + if (!user.info.matrix_user_id) return ; + return

    Todo home route

    ; } diff --git a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx new file mode 100644 index 0000000..7a33ed7 --- /dev/null +++ b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx @@ -0,0 +1,98 @@ +import LinkIcon from "@mui/icons-material/Link"; +import LinkOffIcon from "@mui/icons-material/LinkOff"; +import { + Button, + Card, + CardActions, + CardContent, + Typography, +} from "@mui/material"; +import { MatrixLinkApi } from "../api/MatrixLinkApi"; +import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider"; +import { useLoadingMessage } from "../hooks/contexts_provider/LoadingMessageProvider"; +import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; +import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; + +export function MatrixLinkRoute(): React.ReactElement { + const user = useUserInfo(); + return ( + + {user.info.matrix_user_id === null ? : } + + ); +} + +function ConnectCard(): React.ReactElement { + const alert = useAlert(); + const loadingMessage = useLoadingMessage(); + + const startMatrixConnection = async () => { + try { + loadingMessage.show("Initiating Matrix link..."); + + const res = await MatrixLinkApi.StartAuth(); + + window.location.href = res.url; + } catch (e) { + console.error(`Failed to connect to Matrix account! ${e}`); + alert(`Failed to connect to Matrix account! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + + return ( + + + + Disconnected from your Matrix account + + + + You need to connect MatrixGW to your Matrix account to let it access + your messages. + + + + + + + ); +} + +function ConnectedCard(): React.ReactElement { + const user = useUserInfo(); + + return ( + + + + Connected to your Matrix account + + + +

    + MatrixGW is currently connected to your account with ID{" "} + {user.info.matrix_user_id}. +

    +

    + If you encounter issues with your Matrix account you can try to + disconnect and connect back again. +

    +
    +
    + + + +
    + ); +} diff --git a/matrixgw_frontend/src/widgets/MatrixGWRouteContainer.tsx b/matrixgw_frontend/src/widgets/MatrixGWRouteContainer.tsx new file mode 100644 index 0000000..26c6ba9 --- /dev/null +++ b/matrixgw_frontend/src/widgets/MatrixGWRouteContainer.tsx @@ -0,0 +1,37 @@ +import { Typography } from "@mui/material"; +import React, { type PropsWithChildren } from "react"; + +export function MatrixGWRouteContainer( + p: { + label: string | React.ReactElement; + actions?: React.ReactElement; + } & PropsWithChildren +): React.ReactElement { + return ( +
    +
    + {p.label} + {p.actions ?? <>} +
    + + {p.children} +
    + ); +} diff --git a/matrixgw_frontend/src/widgets/NotLinkedAccountMessage.tsx b/matrixgw_frontend/src/widgets/NotLinkedAccountMessage.tsx new file mode 100644 index 0000000..65412b3 --- /dev/null +++ b/matrixgw_frontend/src/widgets/NotLinkedAccountMessage.tsx @@ -0,0 +1,14 @@ +export function NotLinkedAccountMessage(): React.ReactElement { + return ( +
    + Your Matrix account is not linked yet! +
    + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index 47e8be3..0382979 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -124,7 +124,6 @@ export default function BaseAuthenticatedPage(): React.ReactElement { flexDirection: "column", flex: 1, overflow: "auto", - padding: "50px", }} > @@ -137,6 +136,6 @@ export default function BaseAuthenticatedPage(): React.ReactElement { ); } -export function userUserInfo(): UserInfoContext { +export function useUserInfo(): UserInfoContext { return React.use(UserInfoContextK)!; } diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx index 0f76b97..6beaa76 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx @@ -14,7 +14,7 @@ import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import * as React from "react"; import { RouterLink } from "../RouterLink"; -import { userUserInfo as useUserInfo } from "./BaseAuthenticatedPage"; +import { useUserInfo } from "./BaseAuthenticatedPage"; import ThemeSwitcher from "./ThemeSwitcher"; const AppBar = styled(MuiAppBar)(({ theme }) => ({ diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx index d1584a6..a2c7f3d 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx @@ -107,7 +107,7 @@ export default function DashboardSidebar({ } - href="/matrixlink" + href="/matrix_link" /> Date: Wed, 5 Nov 2025 19:32:11 +0100 Subject: [PATCH 017/124] Can finalize Matrix authentication --- .../src/controllers/auth_controller.rs | 7 +- .../src/controllers/matrix_link_controller.rs | 12 +++ matrixgw_backend/src/controllers/mod.rs | 4 +- .../src/extractors/auth_extractor.rs | 12 +++ .../src/extractors/matrix_client_extractor.rs | 11 +++ matrixgw_backend/src/main.rs | 6 +- .../src/matrix_connection/matrix_client.rs | 31 +++++++- matrixgw_backend/src/users.rs | 10 +-- matrixgw_frontend/src/App.tsx | 2 + matrixgw_frontend/src/api/AuthApi.ts | 1 + matrixgw_frontend/src/api/MatrixLinkApi.ts | 11 +++ .../src/routes/MatrixAuthCallback.tsx | 79 +++++++++++++++++++ .../src/routes/MatrixLinkRoute.tsx | 12 ++- 13 files changed, 180 insertions(+), 18 deletions(-) create mode 100644 matrixgw_frontend/src/routes/MatrixAuthCallback.tsx diff --git a/matrixgw_backend/src/controllers/auth_controller.rs b/matrixgw_backend/src/controllers/auth_controller.rs index 6cbab4c..42d759f 100644 --- a/matrixgw_backend/src/controllers/auth_controller.rs +++ b/matrixgw_backend/src/controllers/auth_controller.rs @@ -1,8 +1,9 @@ use crate::app_config::AppConfig; use crate::controllers::{HttpFailure, HttpResult}; use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use crate::extractors::session_extractor::MatrixGWSession; -use crate::users::{ExtendedUserInfo, User, UserEmail}; +use crate::users::{User, UserEmail}; use actix_remote_ip::RemoteIP; use actix_web::{HttpResponse, web}; use light_openid::primitives::OpenIDConfig; @@ -107,8 +108,8 @@ pub async fn finish_oidc( } /// Get current user information -pub async fn auth_info(auth: AuthExtractor) -> HttpResult { - Ok(HttpResponse::Ok().json(ExtendedUserInfo::from_user(auth.user).await?)) +pub async fn auth_info(client: MatrixClientExtractor) -> HttpResult { + Ok(HttpResponse::Ok().json(client.to_extended_user_info().await?)) } /// Sign out user diff --git a/matrixgw_backend/src/controllers/matrix_link_controller.rs b/matrixgw_backend/src/controllers/matrix_link_controller.rs index 38fea49..084597a 100644 --- a/matrixgw_backend/src/controllers/matrix_link_controller.rs +++ b/matrixgw_backend/src/controllers/matrix_link_controller.rs @@ -1,6 +1,8 @@ use crate::controllers::HttpResult; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use crate::matrix_connection::matrix_client::FinishMatrixAuth; use actix_web::HttpResponse; +use anyhow::Context; #[derive(serde::Serialize)] struct StartAuthResponse { @@ -12,3 +14,13 @@ pub async fn start_auth(client: MatrixClientExtractor) -> HttpResult { let url = client.client.initiate_login().await?.to_string(); Ok(HttpResponse::Ok().json(StartAuthResponse { url })) } + +/// Finish user authentication on Matrix server +pub async fn finish_auth(client: MatrixClientExtractor) -> HttpResult { + client + .client + .finish_login(client.auth.decode_json_body::()?) + .await + .context("Failed to finalize Matrix authentication!")?; + Ok(HttpResponse::Accepted().finish()) +} diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 8a581e9..8ddf218 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -28,7 +28,9 @@ impl ResponseError for HttpFailure { } fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()).body(self.to_string()) + HttpResponse::build(self.status_code()) + .content_type("text/plain") + .body(self.to_string()) } } diff --git a/matrixgw_backend/src/extractors/auth_extractor.rs b/matrixgw_backend/src/extractors/auth_extractor.rs index 8c75a29..36003e6 100644 --- a/matrixgw_backend/src/extractors/auth_extractor.rs +++ b/matrixgw_backend/src/extractors/auth_extractor.rs @@ -11,6 +11,8 @@ use anyhow::Context; use bytes::Bytes; use jwt_simple::common::VerificationOptions; use jwt_simple::prelude::{Duration, HS256Key, MACLike}; +use jwt_simple::reexports::serde_json; +use serde::de::DeserializeOwned; use sha2::{Digest, Sha256}; use std::fmt::Display; use std::net::IpAddr; @@ -32,6 +34,16 @@ pub struct AuthExtractor { pub payload: Option>, } +impl AuthExtractor { + pub fn decode_json_body(&self) -> anyhow::Result { + let payload = self + .payload + .as_ref() + .context("Failed to decode request as json: missing payload!")?; + serde_json::from_slice(payload).context("Failed to decode request json!") + } +} + #[derive(Debug, Eq, PartialEq)] pub struct MatrixJWTKID { pub user_email: UserEmail, diff --git a/matrixgw_backend/src/extractors/matrix_client_extractor.rs b/matrixgw_backend/src/extractors/matrix_client_extractor.rs index fbe6d3c..5de4f7f 100644 --- a/matrixgw_backend/src/extractors/matrix_client_extractor.rs +++ b/matrixgw_backend/src/extractors/matrix_client_extractor.rs @@ -1,6 +1,7 @@ use crate::extractors::auth_extractor::AuthExtractor; use crate::matrix_connection::matrix_client::MatrixClient; use crate::matrix_connection::matrix_manager::MatrixManagerMsg; +use crate::users::ExtendedUserInfo; use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest, web}; use ractor::ActorRef; @@ -10,6 +11,16 @@ pub struct MatrixClientExtractor { pub client: MatrixClient, } +impl MatrixClientExtractor { + pub async fn to_extended_user_info(&self) -> anyhow::Result { + Ok(ExtendedUserInfo { + user: self.auth.user.clone(), + matrix_user_id: self.client.client.user_id().map(|id| id.to_string()), + matrix_device_id: self.client.client.device_id().map(|id| id.to_string()), + }) + } +} + impl FromRequest for MatrixClientExtractor { type Error = actix_web::Error; type Future = futures_util::future::LocalBoxFuture<'static, Result>; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 2571cdc..6c7e4fd 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -55,7 +55,7 @@ async fn main() -> std::io::Result<()> { let cors = Cors::default() .allowed_origin(&AppConfig::get().website_origin) - .allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) + .allowed_methods(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) .allowed_header(constants::API_AUTH_HEADER) .allow_any_header() .supports_credentials() @@ -94,6 +94,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix_link/start_auth", web::post().to(matrix_link_controller::start_auth), ) + .route( + "/api/matrix_link/finish_auth", + web::post().to(matrix_link_controller::finish_auth), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index f6dcc86..c6e88cb 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -1,8 +1,8 @@ use crate::app_config::AppConfig; use crate::users::UserEmail; use crate::utils::rand_utils::rand_string; -use matrix_sdk::authentication::oauth::OAuthError; use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError; +use matrix_sdk::authentication::oauth::{OAuthError, UrlOrQuery}; use matrix_sdk::ruma::serde::Raw; use matrix_sdk::{Client, ClientBuildError}; use url::Url; @@ -28,6 +28,14 @@ enum MatrixClientError { ParseAuthRedirectURL(url::ParseError), #[error("Failed to build auth request! {0}")] BuildAuthRequest(OAuthError), + #[error("Failed to finalize authentication! {0}")] + FinishLogin(matrix_sdk::Error), +} + +#[derive(serde::Deserialize)] +pub struct FinishMatrixAuth { + code: String, + state: String, } #[derive(Clone)] @@ -91,7 +99,7 @@ impl MatrixClient { todo!() } - /// Initiate oauth authentication + /// Initiate OAuth authentication pub async fn initiate_login(&self) -> anyhow::Result { let oauth = self.client.oauth(); @@ -112,4 +120,23 @@ impl MatrixClient { Ok(auth.url) } + + /// Finish OAuth authentication + pub async fn finish_login(&self, info: FinishMatrixAuth) -> anyhow::Result<()> { + let oauth = self.client.oauth(); + oauth + .finish_login(UrlOrQuery::Query(format!( + "state={}&code={}", + info.state, info.code + ))) + .await + .map_err(MatrixClientError::FinishLogin)?; + + log::info!( + "User successfully authenticated as {}!", + self.client.user_id().unwrap() + ); + + Ok(()) + } } diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs index aab18ed..7eca242 100644 --- a/matrixgw_backend/src/users.rs +++ b/matrixgw_backend/src/users.rs @@ -172,13 +172,5 @@ pub struct ExtendedUserInfo { #[serde(flatten)] pub user: User, pub matrix_user_id: Option, -} - -impl ExtendedUserInfo { - pub async fn from_user(user: User) -> anyhow::Result { - Ok(Self { - user, - matrix_user_id: None, // TODO - }) - } + pub matrix_device_id: Option, } diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index 9988388..7cd3001 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -14,6 +14,7 @@ import { MatrixLinkRoute } from "./routes/MatrixLinkRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute"; import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage"; +import { MatrixAuthCallback } from "./routes/MatrixAuthCallback"; interface AuthContext { signedIn: boolean; @@ -39,6 +40,7 @@ export function App(): React.ReactElement { }> } /> } /> + } /> } /> ) : ( diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts index c0fc286..e2e10ac 100644 --- a/matrixgw_frontend/src/api/AuthApi.ts +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -7,6 +7,7 @@ export interface UserInfo { name: string; email: string; matrix_user_id?: string; + matrix_device_id?: string; } const TokenStateKey = "auth-state"; diff --git a/matrixgw_frontend/src/api/MatrixLinkApi.ts b/matrixgw_frontend/src/api/MatrixLinkApi.ts index 6a05a3c..3796189 100644 --- a/matrixgw_frontend/src/api/MatrixLinkApi.ts +++ b/matrixgw_frontend/src/api/MatrixLinkApi.ts @@ -12,4 +12,15 @@ export class MatrixLinkApi { }) ).data; } + + /** + * Finish Matrix Account login + */ + static async FinishAuth(code: string, state: string): Promise { + await APIClient.exec({ + uri: "/matrix_link/finish_auth", + method: "POST", + jsonData: { code, state }, + }); + } } diff --git a/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx new file mode 100644 index 0000000..ae9906c --- /dev/null +++ b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx @@ -0,0 +1,79 @@ +import { Alert, Box, Button, CircularProgress } from "@mui/material"; +import React from "react"; +import { useNavigate, useSearchParams } from "react-router"; +import { MatrixLinkApi } from "../api/MatrixLinkApi"; +import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; +import { RouterLink } from "../widgets/RouterLink"; +import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider"; + +export function MatrixAuthCallback(): React.ReactElement { + const navigate = useNavigate(); + const snackbar = useSnackbar(); + + const info = useUserInfo(); + + const [error, setError] = React.useState(null); + + const [searchParams] = useSearchParams(); + const code = searchParams.get("code"); + const state = searchParams.get("state"); + + const count = React.useRef(""); + + React.useEffect(() => { + const load = async () => { + try { + if (count.current === code) { + return; + } + count.current = code!; + + await MatrixLinkApi.FinishAuth(code!, state!); + info.reloadUserInfo(); + snackbar("Successfully linked to Matrix account!"); + navigate("/matrix_link"); + } catch (e) { + console.error(e); + setError(String(e)); + } + }; + + load(); + }); + + if (error) + return ( + + + Failed to finalize Matrix authentication! +
    +
    + {error} +
    + + +
    + ); + + return ( +
    + +
    + ); +} diff --git a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx index 7a33ed7..23a3b27 100644 --- a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx +++ b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx @@ -79,9 +79,17 @@ function ConnectedCard(): React.ReactElement {

    - MatrixGW is currently connected to your account with ID{" "} - {user.info.matrix_user_id}. + MatrixGW is currently connected to your account with the following + information:

    +
      +
    • + User id: {user.info.matrix_user_id} +
    • +
    • + Device id: {user.info.matrix_device_id} +
    • +

    If you encounter issues with your Matrix account you can try to disconnect and connect back again. From 1438e2de0ea9ae04b5942a57b8f2b6f42725ce97 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 5 Nov 2025 22:53:55 +0100 Subject: [PATCH 018/124] Can save Matrix session after authentication --- matrixgw_backend/Cargo.lock | 1 + matrixgw_backend/Cargo.toml | 3 +- matrixgw_backend/src/app_config.rs | 5 + .../src/controllers/matrix_link_controller.rs | 12 +- .../src/matrix_connection/matrix_client.rs | 115 ++++++++++++++++-- .../src/routes/MatrixAuthCallback.tsx | 6 +- 6 files changed, 124 insertions(+), 18 deletions(-) diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index ebe0055..59988bb 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -3020,6 +3020,7 @@ dependencies = [ "ractor", "rand 0.9.2", "serde", + "serde_json", "sha2", "thiserror 2.0.17", "tokio", diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index 94f3b2b..26e819c 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -30,4 +30,5 @@ hex = "0.4.3" mailchecker = "6.0.19" matrix-sdk = "0.14.0" url = "2.5.7" -ractor = "0.15.9" \ No newline at end of file +ractor = "0.15.9" +serde_json = "1.0.145" \ No newline at end of file diff --git a/matrixgw_backend/src/app_config.rs b/matrixgw_backend/src/app_config.rs index 32fa673..87c3b2c 100644 --- a/matrixgw_backend/src/app_config.rs +++ b/matrixgw_backend/src/app_config.rs @@ -220,6 +220,11 @@ impl AppConfig { pub fn user_matrix_passphrase_path(&self, mail: &UserEmail) -> PathBuf { self.user_directory(mail).join("matrix-db-passphrase") } + + /// Get user Matrix session file path + pub fn user_matrix_session_file_path(&self, mail: &UserEmail) -> PathBuf { + self.user_directory(mail).join("matrix-session.json") + } } #[derive(Debug, Clone, serde::Serialize)] diff --git a/matrixgw_backend/src/controllers/matrix_link_controller.rs b/matrixgw_backend/src/controllers/matrix_link_controller.rs index 084597a..1d769c1 100644 --- a/matrixgw_backend/src/controllers/matrix_link_controller.rs +++ b/matrixgw_backend/src/controllers/matrix_link_controller.rs @@ -2,7 +2,6 @@ use crate::controllers::HttpResult; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use crate::matrix_connection::matrix_client::FinishMatrixAuth; use actix_web::HttpResponse; -use anyhow::Context; #[derive(serde::Serialize)] struct StartAuthResponse { @@ -17,10 +16,15 @@ pub async fn start_auth(client: MatrixClientExtractor) -> HttpResult { /// Finish user authentication on Matrix server pub async fn finish_auth(client: MatrixClientExtractor) -> HttpResult { - client + match client .client .finish_login(client.auth.decode_json_body::()?) .await - .context("Failed to finalize Matrix authentication!")?; - Ok(HttpResponse::Accepted().finish()) + { + Ok(_) => Ok(HttpResponse::Accepted().finish()), + Err(e) => { + log::error!("Failed to finish Matrix authentication: {e}"); + Err(e.into()) + } + } } diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index c6e88cb..dafb0c2 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -1,15 +1,29 @@ use crate::app_config::AppConfig; use crate::users::UserEmail; use crate::utils::rand_utils::rand_string; +use anyhow::Context; use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError; -use matrix_sdk::authentication::oauth::{OAuthError, UrlOrQuery}; +use matrix_sdk::authentication::oauth::{ClientId, OAuthError, UrlOrQuery, UserSession}; use matrix_sdk::ruma::serde::Raw; use matrix_sdk::{Client, ClientBuildError}; +use serde::{Deserialize, Serialize}; use url::Url; +/// The full session to persist. +#[derive(Debug, Serialize, Deserialize, Clone)] +struct StoredSession { + /// The OAuth 2.0 user session. + user_session: UserSession, + + /// The OAuth 2.0 client ID. + client_id: ClientId, +} + /// Matrix Gateway session errors #[derive(thiserror::Error, Debug)] enum MatrixClientError { + #[error("Failed to destroy previous client data! {0}")] + DestroyPreviousData(Box), #[error("Failed to create Matrix database storage directory! {0}")] CreateMatrixDbDir(std::io::Error), #[error("Failed to create database passphrase! {0}")] @@ -18,6 +32,8 @@ enum MatrixClientError { ReadDbPassphrase(std::io::Error), #[error("Failed to build Matrix client! {0}")] BuildMatrixClient(ClientBuildError), + #[error("Failed to clear Matrix session file! {0}")] + ClearMatrixSessionFile(std::io::Error), #[error("Failed to clear Matrix database storage directory! {0}")] ClearMatrixDbDir(std::io::Error), #[error("Failed to remove database passphrase! {0}")] @@ -30,6 +46,8 @@ enum MatrixClientError { BuildAuthRequest(OAuthError), #[error("Failed to finalize authentication! {0}")] FinishLogin(matrix_sdk::Error), + #[error("Failed to write session file! {0}")] + WriteSessionFile(std::io::Error), } #[derive(serde::Deserialize)] @@ -47,6 +65,15 @@ pub struct MatrixClient { impl MatrixClient { /// Start to build Matrix client to initiate user authentication pub async fn build_client(email: &UserEmail) -> anyhow::Result { + // Check if we are restoring a previous state + let is_restoring = AppConfig::get() + .user_matrix_session_file_path(email) + .is_file(); + if !is_restoring { + Self::destroy_data(email).map_err(MatrixClientError::DestroyPreviousData)?; + } + + // Determine Matrix database path let db_path = AppConfig::get().user_matrix_db_path(email); std::fs::create_dir_all(&db_path).map_err(MatrixClientError::CreateMatrixDbDir)?; @@ -69,34 +96,46 @@ impl MatrixClient { .map_err(MatrixClientError::BuildMatrixClient)?; // Check metadata - let server_metadata = client - .oauth() + let oauth = client.oauth(); + let server_metadata = oauth .server_metadata() .await .map_err(MatrixClientError::FetchServerMetadata)?; log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer); + // TODO : restore client ID to oauth if needed // TODO : restore client if client already existed - Ok(Self { + let client = Self { email: email.clone(), client, - }) + }; + + // Automatically save session when token gets refreshed + client.setup_background_session_save().await; + + Ok(client) } - /// Destroy this Matrix client instance - pub fn destroy(&self) -> anyhow::Result<()> { - let db_path = AppConfig::get().user_matrix_db_path(&self.email); - if db_path.is_file() { + /// Destroy Matrix client related data + fn destroy_data(email: &UserEmail) -> anyhow::Result<(), Box> { + let session_path = AppConfig::get().user_matrix_session_file_path(email); + if session_path.is_file() { + std::fs::remove_file(&session_path) + .map_err(MatrixClientError::ClearMatrixSessionFile)?; + } + + let db_path = AppConfig::get().user_matrix_db_path(email); + if db_path.is_dir() { std::fs::remove_dir_all(&db_path).map_err(MatrixClientError::ClearMatrixDbDir)?; } - let passphrase_path = AppConfig::get().user_matrix_passphrase_path(&self.email); + let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email); if passphrase_path.is_file() { std::fs::remove_file(passphrase_path).map_err(MatrixClientError::ClearDbPassphrase)?; } - todo!() + Ok(()) } /// Initiate OAuth authentication @@ -137,6 +176,60 @@ impl MatrixClient { self.client.user_id().unwrap() ); + // Persist session tokens + self.save_stored_session().await?; + + Ok(()) + } + + /// Automatically persist session onto disk + pub async fn setup_background_session_save(&self) { + let this = self.clone(); + tokio::spawn(async move { + while let Ok(update) = this.client.subscribe_to_session_changes().recv().await { + match update { + matrix_sdk::SessionChange::UnknownToken { soft_logout } => { + log::warn!("Received an unknown token error; soft logout? {soft_logout:?}"); + } + matrix_sdk::SessionChange::TokensRefreshed => { + // The tokens have been refreshed, persist them to disk. + if let Err(err) = this.save_stored_session().await { + log::error!("Unable to store a session in the background: {err}"); + } + } + } + } + }); + } + + /// Update the session stored on the filesystem. + async fn save_stored_session(&self) -> anyhow::Result<()> { + log::debug!("Save the stored session for {:?}...", self.email); + + let user_session: UserSession = self + .client + .oauth() + .user_session() + .context("A logged in client must have a session")?; + + let stored_session = StoredSession { + user_session, + client_id: self + .client + .oauth() + .client_id() + .context("Client ID should be set at this point!")? + .clone(), + }; + + let serialized_session = serde_json::to_string(&stored_session)?; + std::fs::write( + AppConfig::get().user_matrix_session_file_path(&self.email), + serialized_session, + ) + .map_err(MatrixClientError::WriteSessionFile)?; + + log::debug!("Updating the stored session: done!"); Ok(()) } } diff --git a/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx index ae9906c..3eafadc 100644 --- a/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx +++ b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx @@ -29,17 +29,19 @@ export function MatrixAuthCallback(): React.ReactElement { count.current = code!; await MatrixLinkApi.FinishAuth(code!, state!); - info.reloadUserInfo(); + snackbar("Successfully linked to Matrix account!"); navigate("/matrix_link"); } catch (e) { console.error(e); setError(String(e)); + } finally { + info.reloadUserInfo(); } }; load(); - }); + }, [code, state]); if (error) return ( From 1ba5372468a752fd8df53a6afb1110bf4d3be977 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 6 Nov 2025 18:58:43 +0100 Subject: [PATCH 019/124] Restore user session on restart --- .../src/matrix_connection/matrix_client.rs | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index dafb0c2..e682292 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -3,7 +3,9 @@ use crate::users::UserEmail; use crate::utils::rand_utils::rand_string; use anyhow::Context; use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError; -use matrix_sdk::authentication::oauth::{ClientId, OAuthError, UrlOrQuery, UserSession}; +use matrix_sdk::authentication::oauth::{ + ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession, +}; use matrix_sdk::ruma::serde::Raw; use matrix_sdk::{Client, ClientBuildError}; use serde::{Deserialize, Serialize}; @@ -40,6 +42,12 @@ enum MatrixClientError { ClearDbPassphrase(std::io::Error), #[error("Failed to fetch server metadata! {0}")] FetchServerMetadata(OAuthDiscoveryError), + #[error("Failed to load stored session! {0}")] + LoadStoredSession(std::io::Error), + #[error("Failed to decode stored session! {0}")] + DecodeStoredSession(serde_json::Error), + #[error("Failed to restore stored session! {0}")] + RestoreSession(matrix_sdk::Error), #[error("Failed to parse auth redirect URL! {0}")] ParseAuthRedirectURL(url::ParseError), #[error("Failed to build auth request! {0}")] @@ -66,9 +74,8 @@ impl MatrixClient { /// Start to build Matrix client to initiate user authentication pub async fn build_client(email: &UserEmail) -> anyhow::Result { // Check if we are restoring a previous state - let is_restoring = AppConfig::get() - .user_matrix_session_file_path(email) - .is_file(); + let session_file_path = AppConfig::get().user_matrix_session_file_path(email); + let is_restoring = session_file_path.is_file(); if !is_restoring { Self::destroy_data(email).map_err(MatrixClientError::DestroyPreviousData)?; } @@ -103,8 +110,23 @@ impl MatrixClient { .map_err(MatrixClientError::FetchServerMetadata)?; log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer); - // TODO : restore client ID to oauth if needed - // TODO : restore client if client already existed + if is_restoring { + let session: StoredSession = serde_json::from_str( + std::fs::read_to_string(session_file_path) + .map_err(MatrixClientError::LoadStoredSession)? + .as_str(), + ) + .map_err(MatrixClientError::DecodeStoredSession)?; + + // Restore data + client + .restore_session(OAuthSession { + client_id: session.client_id, + user: session.user_session, + }) + .await + .map_err(MatrixClientError::RestoreSession)?; + } let client = Self { email: email.clone(), From 8bbbe7022fe4665d7f69fe1a4756d875527e7037 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 6 Nov 2025 21:18:27 +0100 Subject: [PATCH 020/124] Automatically disconnect user when token is invalid --- matrixgw_backend/src/broadcast_messages.rs | 10 +++ matrixgw_backend/src/lib.rs | 1 + matrixgw_backend/src/main.rs | 6 +- .../src/matrix_connection/matrix_client.rs | 70 ++++++++++++++++--- .../src/matrix_connection/matrix_manager.rs | 27 +++++-- 5 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 matrixgw_backend/src/broadcast_messages.rs diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs new file mode 100644 index 0000000..423114f --- /dev/null +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -0,0 +1,10 @@ +use crate::users::UserEmail; + +pub type BroadcastSender = tokio::sync::broadcast::Sender; + +/// Broadcast messages +#[derive(Debug, Clone)] +pub enum BroadcastMessage { + /// User is or has been disconnected + UserDisconnected(UserEmail), +} diff --git a/matrixgw_backend/src/lib.rs b/matrixgw_backend/src/lib.rs index 61b9e52..3eb69b6 100644 --- a/matrixgw_backend/src/lib.rs +++ b/matrixgw_backend/src/lib.rs @@ -1,4 +1,5 @@ pub mod app_config; +pub mod broadcast_messages; pub mod constants; pub mod controllers; pub mod extractors; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 6c7e4fd..7614ee8 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -7,6 +7,7 @@ use actix_web::cookie::Key; use actix_web::middleware::Logger; use actix_web::{App, HttpServer, web}; use matrixgw_backend::app_config::AppConfig; +use matrixgw_backend::broadcast_messages::BroadcastMessage; use matrixgw_backend::constants; use matrixgw_backend::controllers::{auth_controller, matrix_link_controller, server_controller}; use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor; @@ -24,6 +25,8 @@ async fn main() -> std::io::Result<()> { .await .expect("Failed to connect to Redis!"); + let (ws_tx, _) = tokio::sync::broadcast::channel::(16); + // Auto create default account, if requested if let Some(mail) = &AppConfig::get().unsecure_auto_login_email() { User::create_or_update_user(mail, "Anonymous") @@ -35,7 +38,7 @@ async fn main() -> std::io::Result<()> { let (manager_actor, manager_actor_handle) = Actor::spawn( Some("matrix-clients-manager".to_string()), MatrixManagerActor, - (), + ws_tx.clone(), ) .await .expect("Failed to start Matrix manager actor!"); @@ -69,6 +72,7 @@ async fn main() -> std::io::Result<()> { .app_data(web::Data::new(RemoteIPConfig { proxy: AppConfig::get().proxy_ip.clone(), })) + .app_data(web::Data::new(ws_tx.clone())) // Server controller .route("/robots.txt", web::get().to(server_controller::robots_txt)) .route( diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index e682292..cfc23b1 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -1,13 +1,17 @@ use crate::app_config::AppConfig; +use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use crate::users::UserEmail; use crate::utils::rand_utils::rand_string; use anyhow::Context; -use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError; +use matrix_sdk::authentication::oauth::error::{ + BasicErrorResponseType, OAuthDiscoveryError, RequestTokenError, +}; use matrix_sdk::authentication::oauth::{ ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession, }; use matrix_sdk::ruma::serde::Raw; -use matrix_sdk::{Client, ClientBuildError}; +use matrix_sdk::{Client, ClientBuildError, RefreshTokenError}; +use ractor::ActorRef; use serde::{Deserialize, Serialize}; use url::Url; @@ -48,6 +52,10 @@ enum MatrixClientError { DecodeStoredSession(serde_json::Error), #[error("Failed to restore stored session! {0}")] RestoreSession(matrix_sdk::Error), + #[error("Failed to disconnect user! {0}")] + DisconnectUser(anyhow::Error), + #[error("Failed to refresh access token! {0}")] + InitialRefreshToken(RefreshTokenError), #[error("Failed to parse auth redirect URL! {0}")] ParseAuthRedirectURL(url::ParseError), #[error("Failed to build auth request! {0}")] @@ -66,13 +74,17 @@ pub struct FinishMatrixAuth { #[derive(Clone)] pub struct MatrixClient { + manager: ActorRef, pub email: UserEmail, pub client: Client, } impl MatrixClient { /// Start to build Matrix client to initiate user authentication - pub async fn build_client(email: &UserEmail) -> anyhow::Result { + pub async fn build_client( + manager: ActorRef, + email: &UserEmail, + ) -> anyhow::Result { // Check if we are restoring a previous state let session_file_path = AppConfig::get().user_matrix_session_file_path(email); let is_restoring = session_file_path.is_file(); @@ -102,8 +114,14 @@ impl MatrixClient { .await .map_err(MatrixClientError::BuildMatrixClient)?; + let client = Self { + manager, + email: email.clone(), + client, + }; + // Check metadata - let oauth = client.oauth(); + let oauth = client.client.oauth(); let server_metadata = oauth .server_metadata() .await @@ -118,20 +136,33 @@ impl MatrixClient { ) .map_err(MatrixClientError::DecodeStoredSession)?; - // Restore data + // Restore session client + .client .restore_session(OAuthSession { client_id: session.client_id, user: session.user_session, }) .await .map_err(MatrixClientError::RestoreSession)?; - } - let client = Self { - email: email.clone(), - client, - }; + // Force token refresh to make sure session is still alive, otherwise disconnect user + if let Err(refresh_error) = client.client.oauth().refresh_access_token().await { + if let RefreshTokenError::OAuth(e) = &refresh_error + && let OAuthError::RefreshToken(RequestTokenError::ServerResponse(e)) = &**e + && e.error() == &BasicErrorResponseType::InvalidGrant + { + log::warn!( + "Refresh token rejected by server, token must have been invalidated! {refresh_error}" + ); + client + .disconnect() + .await + .map_err(MatrixClientError::DisconnectUser)?; + } + return Err(MatrixClientError::InitialRefreshToken(refresh_error).into()); + } + } // Automatically save session when token gets refreshed client.setup_background_session_save().await; @@ -212,6 +243,13 @@ impl MatrixClient { match update { matrix_sdk::SessionChange::UnknownToken { soft_logout } => { log::warn!("Received an unknown token error; soft logout? {soft_logout:?}"); + if let Err(e) = this + .manager + .cast(MatrixManagerMsg::DisconnectClient(this.email)) + { + log::warn!("Failed to propagate invalid token error: {e}"); + } + break; } matrix_sdk::SessionChange::TokensRefreshed => { // The tokens have been refreshed, persist them to disk. @@ -254,4 +292,16 @@ impl MatrixClient { log::debug!("Updating the stored session: done!"); Ok(()) } + + /// Disconnect user from client + pub async fn disconnect(self) -> anyhow::Result<()> { + if let Err(e) = self.client.logout().await { + log::warn!("Failed to send logout request: {e}"); + } + + // Destroy user associated data + Self::destroy_data(&self.email)?; + + Ok(()) + } } diff --git a/matrixgw_backend/src/matrix_connection/matrix_manager.rs b/matrixgw_backend/src/matrix_connection/matrix_manager.rs index d6f180a..d75cab6 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_manager.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -1,14 +1,17 @@ +use crate::broadcast_messages::{BroadcastMessage, BroadcastSender}; use crate::matrix_connection::matrix_client::MatrixClient; use crate::users::UserEmail; use ractor::{Actor, ActorProcessingErr, ActorRef, RpcReplyPort}; use std::collections::HashMap; pub struct MatrixManagerState { + pub broadcast_sender: BroadcastSender, pub clients: HashMap, } pub enum MatrixManagerMsg { GetClient(UserEmail, RpcReplyPort>), + DisconnectClient(UserEmail), } pub struct MatrixManagerActor; @@ -16,21 +19,22 @@ pub struct MatrixManagerActor; impl Actor for MatrixManagerActor { type Msg = MatrixManagerMsg; type State = MatrixManagerState; - type Arguments = (); + type Arguments = BroadcastSender; async fn pre_start( &self, _myself: ActorRef, - _args: Self::Arguments, + args: Self::Arguments, ) -> Result { Ok(MatrixManagerState { + broadcast_sender: args, clients: HashMap::new(), }) } async fn handle( &self, - _myself: ActorRef, + myself: ActorRef, message: Self::Msg, state: &mut Self::State, ) -> Result<(), ActorProcessingErr> { @@ -41,7 +45,7 @@ impl Actor for MatrixManagerActor { None => { // Generate client if required log::info!("Building new client for {:?}", &email); - match MatrixClient::build_client(&email).await { + match MatrixClient::build_client(myself, &email).await { Ok(c) => { state.clients.insert(email.clone(), c.clone()); Ok(c) @@ -56,6 +60,21 @@ impl Actor for MatrixManagerActor { log::warn!("Failed to send client information: {e}") } } + MatrixManagerMsg::DisconnectClient(email) => { + if let Some(c) = state.clients.remove(&email) { + if let Err(e) = c.disconnect().await { + log::error!("Failed to disconnect client: {e}"); + } + if let Err(e) = state + .broadcast_sender + .send(BroadcastMessage::UserDisconnected(email)) + { + log::warn!( + "Failed to notify that user has been disconnected from Matrix! {e}" + ); + } + } + } } Ok(()) } From 70a246355b538c06d58f315d1826ae452edcbdd7 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 6 Nov 2025 21:33:09 +0100 Subject: [PATCH 021/124] Can disconnect user from UI --- .../src/controllers/matrix_link_controller.rs | 17 +++++++++- matrixgw_backend/src/main.rs | 4 +++ matrixgw_frontend/src/api/MatrixLinkApi.ts | 10 ++++++ .../src/routes/MatrixLinkRoute.tsx | 33 ++++++++++++++++++- 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix_link_controller.rs b/matrixgw_backend/src/controllers/matrix_link_controller.rs index 1d769c1..16fe6cf 100644 --- a/matrixgw_backend/src/controllers/matrix_link_controller.rs +++ b/matrixgw_backend/src/controllers/matrix_link_controller.rs @@ -1,7 +1,10 @@ use crate::controllers::HttpResult; +use crate::extractors::auth_extractor::AuthExtractor; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use crate::matrix_connection::matrix_client::FinishMatrixAuth; -use actix_web::HttpResponse; +use crate::matrix_connection::matrix_manager::MatrixManagerMsg; +use actix_web::{HttpResponse, web}; +use ractor::ActorRef; #[derive(serde::Serialize)] struct StartAuthResponse { @@ -28,3 +31,15 @@ pub async fn finish_auth(client: MatrixClientExtractor) -> HttpResult { } } } + +/// Logout user from Matrix server +pub async fn logout( + auth: AuthExtractor, + manager: web::Data>, +) -> HttpResult { + manager + .cast(MatrixManagerMsg::DisconnectClient(auth.user.email)) + .expect("Failed to communicate with matrix manager!"); + + Ok(HttpResponse::Ok().finish()) +} diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 7614ee8..a239e35 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -102,6 +102,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix_link/finish_auth", web::post().to(matrix_link_controller::finish_auth), ) + .route( + "/api/matrix_link/logout", + web::post().to(matrix_link_controller::logout), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_frontend/src/api/MatrixLinkApi.ts b/matrixgw_frontend/src/api/MatrixLinkApi.ts index 3796189..d5b3cb7 100644 --- a/matrixgw_frontend/src/api/MatrixLinkApi.ts +++ b/matrixgw_frontend/src/api/MatrixLinkApi.ts @@ -23,4 +23,14 @@ export class MatrixLinkApi { jsonData: { code, state }, }); } + + /** + * Disconnect from Matrix Account + */ + static async Disconnect(): Promise { + await APIClient.exec({ + uri: "/matrix_link/logout", + method: "POST", + }); + } } diff --git a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx index 23a3b27..3e9f394 100644 --- a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx +++ b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx @@ -9,7 +9,9 @@ import { } from "@mui/material"; import { MatrixLinkApi } from "../api/MatrixLinkApi"; import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider"; +import { useConfirm } from "../hooks/contexts_provider/ConfirmDialogProvider"; import { useLoadingMessage } from "../hooks/contexts_provider/LoadingMessageProvider"; +import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider"; import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; @@ -68,8 +70,32 @@ function ConnectCard(): React.ReactElement { } function ConnectedCard(): React.ReactElement { + const snackbar = useSnackbar(); + const confirm = useConfirm(); + const alert = useAlert(); + const loadingMessage = useLoadingMessage(); + + const info = useUserInfo(); + const user = useUserInfo(); + const handleDisconnect = async () => { + if (!(await confirm("Do you really want to unlink your Matrix account?"))) + return; + + try { + loadingMessage.show("Unlinking Matrix account..."); + await MatrixLinkApi.Disconnect(); + snackbar("Successfully unlinked Matrix account!"); + } catch (e) { + console.error(`Failed to unlink user account! ${e}`); + alert(`Failed to unlink your account! ${e}`); + } finally { + info.reloadUserInfo(); + loadingMessage.hide(); + } + }; + return ( @@ -97,7 +123,12 @@ function ConnectedCard(): React.ReactElement { - From 4a72411d6578ddda676362dd18e1cdbc06d6e23e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 10 Nov 2025 08:32:17 +0100 Subject: [PATCH 022/124] Return encryption recovery status on API --- .../src/extractors/matrix_client_extractor.rs | 1 + .../src/matrix_connection/matrix_client.rs | 34 +++++++++++++++++++ matrixgw_backend/src/users.rs | 2 ++ matrixgw_frontend/src/api/AuthApi.ts | 1 + 4 files changed, 38 insertions(+) diff --git a/matrixgw_backend/src/extractors/matrix_client_extractor.rs b/matrixgw_backend/src/extractors/matrix_client_extractor.rs index 5de4f7f..545db6c 100644 --- a/matrixgw_backend/src/extractors/matrix_client_extractor.rs +++ b/matrixgw_backend/src/extractors/matrix_client_extractor.rs @@ -17,6 +17,7 @@ impl MatrixClientExtractor { user: self.auth.user.clone(), matrix_user_id: self.client.client.user_id().map(|id| id.to_string()), matrix_device_id: self.client.client.device_id().map(|id| id.to_string()), + matrix_recovery_state: self.client.recovery_state(), }) } } diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index cfc23b1..59b342c 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -9,6 +9,7 @@ use matrix_sdk::authentication::oauth::error::{ use matrix_sdk::authentication::oauth::{ ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession, }; +use matrix_sdk::encryption::recovery::RecoveryState; use matrix_sdk::ruma::serde::Raw; use matrix_sdk::{Client, ClientBuildError, RefreshTokenError}; use ractor::ActorRef; @@ -25,6 +26,14 @@ struct StoredSession { client_id: ClientId, } +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +pub enum EncryptionRecoveryState { + Unknown, + Enabled, + Disabled, + Incomplete, +} + /// Matrix Gateway session errors #[derive(thiserror::Error, Debug)] enum MatrixClientError { @@ -64,6 +73,8 @@ enum MatrixClientError { FinishLogin(matrix_sdk::Error), #[error("Failed to write session file! {0}")] WriteSessionFile(std::io::Error), + #[error("Failed to rename device! {0}")] + RenameDevice(matrix_sdk::HttpError), } #[derive(serde::Deserialize)] @@ -232,6 +243,19 @@ impl MatrixClient { // Persist session tokens self.save_stored_session().await?; + // Rename created session to give it a more explicit name + self.client + .rename_device( + self.client + .session_meta() + .context("Missing device ID!")? + .device_id + .as_ref(), + &AppConfig::get().website_origin, + ) + .await + .map_err(MatrixClientError::RenameDevice)?; + Ok(()) } @@ -304,4 +328,14 @@ impl MatrixClient { Ok(()) } + + /// Get current encryption keys recovery state + pub fn recovery_state(&self) -> EncryptionRecoveryState { + match self.client.encryption().recovery().state() { + RecoveryState::Unknown => EncryptionRecoveryState::Unknown, + RecoveryState::Enabled => EncryptionRecoveryState::Enabled, + RecoveryState::Disabled => EncryptionRecoveryState::Disabled, + RecoveryState::Incomplete => EncryptionRecoveryState::Incomplete, + } + } } diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs index 7eca242..3dd37da 100644 --- a/matrixgw_backend/src/users.rs +++ b/matrixgw_backend/src/users.rs @@ -1,4 +1,5 @@ use crate::app_config::AppConfig; +use crate::matrix_connection::matrix_client::EncryptionRecoveryState; use crate::utils::time_utils::time_secs; use jwt_simple::reexports::serde_json; use std::cmp::min; @@ -173,4 +174,5 @@ pub struct ExtendedUserInfo { pub user: User, pub matrix_user_id: Option, pub matrix_device_id: Option, + pub matrix_recovery_state: EncryptionRecoveryState, } diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts index e2e10ac..e504b25 100644 --- a/matrixgw_frontend/src/api/AuthApi.ts +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -8,6 +8,7 @@ export interface UserInfo { email: string; matrix_user_id?: string; matrix_device_id?: string; + matrix_recovery_state?: string; } const TokenStateKey = "auth-state"; From a23d671376ff461fddb49e8dfd4988b929ce8143 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 10 Nov 2025 08:47:02 +0100 Subject: [PATCH 023/124] Can set recovery key of user --- .../src/controllers/matrix_link_controller.rs | 14 ++++++++++++++ matrixgw_backend/src/extractors/auth_extractor.rs | 2 +- matrixgw_backend/src/main.rs | 4 ++++ .../src/matrix_connection/matrix_client.rs | 13 +++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/matrixgw_backend/src/controllers/matrix_link_controller.rs b/matrixgw_backend/src/controllers/matrix_link_controller.rs index 16fe6cf..d4e507e 100644 --- a/matrixgw_backend/src/controllers/matrix_link_controller.rs +++ b/matrixgw_backend/src/controllers/matrix_link_controller.rs @@ -43,3 +43,17 @@ pub async fn logout( Ok(HttpResponse::Ok().finish()) } + +#[derive(serde::Deserialize)] +struct SetRecoveryKeyRequest { + key: String, +} + +/// Set recovery key of user +pub async fn set_recovery_key(client: MatrixClientExtractor) -> HttpResult { + let key = client.auth.decode_json_body::()?.key; + + client.client.set_recovery_key(&key).await?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/matrixgw_backend/src/extractors/auth_extractor.rs b/matrixgw_backend/src/extractors/auth_extractor.rs index 36003e6..277ffd6 100644 --- a/matrixgw_backend/src/extractors/auth_extractor.rs +++ b/matrixgw_backend/src/extractors/auth_extractor.rs @@ -40,7 +40,7 @@ impl AuthExtractor { .payload .as_ref() .context("Failed to decode request as json: missing payload!")?; - serde_json::from_slice(payload).context("Failed to decode request json!") + Ok(serde_json::from_slice(payload)?) } } diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index a239e35..f0af96a 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -106,6 +106,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix_link/logout", web::post().to(matrix_link_controller::logout), ) + .route( + "/api/matrix_link/set_recovery_key", + web::post().to(matrix_link_controller::set_recovery_key), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index 59b342c..d846e76 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -75,6 +75,8 @@ enum MatrixClientError { WriteSessionFile(std::io::Error), #[error("Failed to rename device! {0}")] RenameDevice(matrix_sdk::HttpError), + #[error("Failed to set recovery key! {0}")] + SetRecoveryKey(matrix_sdk::encryption::recovery::RecoveryError), } #[derive(serde::Deserialize)] @@ -338,4 +340,15 @@ impl MatrixClient { RecoveryState::Incomplete => EncryptionRecoveryState::Incomplete, } } + + /// Set new encryption key recovery key + pub async fn set_recovery_key(&self, key: &str) -> anyhow::Result<()> { + Ok(self + .client + .encryption() + .recovery() + .recover(key) + .await + .map_err(MatrixClientError::SetRecoveryKey)?) + } } From 84c90ea03369d2dab4d20b3f0097c4c7d204e64b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 10 Nov 2025 17:42:32 +0100 Subject: [PATCH 024/124] Can set user recovery key from UI --- .../src/matrix_connection/matrix_client.rs | 7 + matrixgw_frontend/src/api/AuthApi.ts | 2 +- matrixgw_frontend/src/api/MatrixLinkApi.ts | 11 ++ .../LoadingMessageProvider.tsx | 9 +- .../src/routes/MatrixLinkRoute.tsx | 124 +++++++++++++++++- .../dashboard/BaseAuthenticatedPage.tsx | 25 +++- 6 files changed, 162 insertions(+), 16 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index d846e76..3eca30f 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -159,6 +159,13 @@ impl MatrixClient { .await .map_err(MatrixClientError::RestoreSession)?; + // Wait for encryption tasks to complete + client + .client + .encryption() + .wait_for_e2ee_initialization_tasks() + .await; + // Force token refresh to make sure session is still alive, otherwise disconnect user if let Err(refresh_error) = client.client.oauth().refresh_access_token().await { if let RefreshTokenError::OAuth(e) = &refresh_error diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts index e504b25..c9d0fa8 100644 --- a/matrixgw_frontend/src/api/AuthApi.ts +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -8,7 +8,7 @@ export interface UserInfo { email: string; matrix_user_id?: string; matrix_device_id?: string; - matrix_recovery_state?: string; + matrix_recovery_state?: "Enabled" | "Disabled" | "Unknown" | "Incomplete"; } const TokenStateKey = "auth-state"; diff --git a/matrixgw_frontend/src/api/MatrixLinkApi.ts b/matrixgw_frontend/src/api/MatrixLinkApi.ts index d5b3cb7..12507b7 100644 --- a/matrixgw_frontend/src/api/MatrixLinkApi.ts +++ b/matrixgw_frontend/src/api/MatrixLinkApi.ts @@ -33,4 +33,15 @@ export class MatrixLinkApi { method: "POST", }); } + + /** + * Set a new user recovery key + */ + static async SetRecoveryKey(key: string): Promise { + await APIClient.exec({ + uri: "/matrix_link/set_recovery_key", + method: "POST", + jsonData: { key }, + }); + } } diff --git a/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx b/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx index 083a985..6338de7 100644 --- a/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx +++ b/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx @@ -17,18 +17,17 @@ const LoadingMessageContextK = export function LoadingMessageProvider( p: PropsWithChildren ): React.ReactElement { - const [open, setOpen] = React.useState(false); + const [open, setOpen] = React.useState(0); const [message, setMessage] = React.useState(""); const hook: LoadingMessageContext = { show(message) { setMessage(message); - setOpen(true); + setOpen((v) => v + 1); }, hide() { - setMessage(""); - setOpen(false); + setOpen((v) => v - 1); }, }; @@ -36,7 +35,7 @@ export function LoadingMessageProvider( <> {p.children} -

    + 0}>
    - {user.info.matrix_user_id === null ? : } + {user.info.matrix_user_id === null ? ( + + ) : ( + <> + + + + )} ); } @@ -75,8 +92,6 @@ function ConnectedCard(): React.ReactElement { const alert = useAlert(); const loadingMessage = useLoadingMessage(); - const info = useUserInfo(); - const user = useUserInfo(); const handleDisconnect = async () => { @@ -91,13 +106,13 @@ function ConnectedCard(): React.ReactElement { console.error(`Failed to unlink user account! ${e}`); alert(`Failed to unlink your account! ${e}`); } finally { - info.reloadUserInfo(); + user.reloadUserInfo(); loadingMessage.hide(); } }; return ( - + Connected to your Matrix account @@ -135,3 +150,102 @@ function ConnectedCard(): React.ReactElement { ); } + +function EncryptionKeyStatus(): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + const loadingMessage = useLoadingMessage(); + + const user = useUserInfo(); + + const [typeNewKey, setTypeNewKey] = React.useState(false); + const [newKey, setNewKey] = React.useState(""); + + const handleSetKey = () => setTypeNewKey(true); + const cancelSetKey = () => setTypeNewKey(false); + const handleSubmitKey = async () => { + try { + loadingMessage.show("Updating recovery key..."); + + await MatrixLinkApi.SetRecoveryKey(newKey); + setNewKey(""); + setTypeNewKey(false); + + snackbar("Recovery key successfully updated!"); + user.reloadUserInfo(); + } catch (e) { + console.error(`Failed to set new recovery key! ${e}`); + alert(`Failed to set new recovery key! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + + return ( + <> + + + + Recovery keys + + +

    + Recovery key is used to verify MatrixGW connection and access + message history in encrypted rooms. +

    + +

    + Current encryption status:{" "} + {user.info.matrix_recovery_state === "Enabled" ? ( + + ) : ( + + )}{" "} + {user.info.matrix_recovery_state} +

    +
    +
    + + + +
    + + {/* Set new key dialog */} + + Set new recovery key + + + Enter below you recovery key to verify this session and gain access + to old messages. + + setNewKey(e.target.value)} + fullWidth + /> + + + + + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index 0382979..17d9f2a 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -1,15 +1,17 @@ +import { Button } from "@mui/material"; import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import Toolbar from "@mui/material/Toolbar"; import useMediaQuery from "@mui/material/useMediaQuery"; import * as React from "react"; import { Outlet, useNavigate } from "react-router"; +import { AuthApi, type UserInfo } from "../../api/AuthApi"; +import { useAuth } from "../../App"; +import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider"; +import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider"; +import { AsyncWidget } from "../AsyncWidget"; import DashboardHeader from "./DashboardHeader"; import DashboardSidebar from "./DashboardSidebar"; -import { AuthApi, type UserInfo } from "../../api/AuthApi"; -import { AsyncWidget } from "../AsyncWidget"; -import { Button } from "@mui/material"; -import { useAuth } from "../../App"; interface UserInfoContext { info: UserInfo; @@ -21,12 +23,25 @@ const UserInfoContextK = React.createContext(null); export default function BaseAuthenticatedPage(): React.ReactElement { const theme = useTheme(); + const alert = useAlert(); + const loadingMessage = useLoadingMessage(); const [userInfo, setuserInfo] = React.useState(null); const loadUserInfo = async () => { setuserInfo(await AuthApi.GetUserInfo()); }; + const reloadUserInfo = async () => { + try { + loadingMessage.show("Refreshing user information..."); + } catch (e) { + console.error(`Failed to load user information! ${e}`); + alert(`Failed to load user information! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + const auth = useAuth(); const navigate = useNavigate(); @@ -85,7 +100,7 @@ export default function BaseAuthenticatedPage(): React.ReactElement { From 7925785c8bf25293f588e00992ea79712579e977 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 11 Nov 2025 17:19:23 +0100 Subject: [PATCH 025/124] Fix issue --- .../src/matrix_connection/matrix_client.rs | 11 ++++++----- .../src/widgets/dashboard/BaseAuthenticatedPage.tsx | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index 3eca30f..c06f7a0 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -175,12 +175,13 @@ impl MatrixClient { log::warn!( "Refresh token rejected by server, token must have been invalidated! {refresh_error}" ); - client - .disconnect() - .await - .map_err(MatrixClientError::DisconnectUser)?; + // TODO : resolve + /*client + .disconnect() + .await + .map_err(MatrixClientError::DisconnectUser)?;*/ } - return Err(MatrixClientError::InitialRefreshToken(refresh_error).into()); + //return Err(MatrixClientError::InitialRefreshToken(refresh_error).into()); } } diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index 17d9f2a..338fda7 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -34,6 +34,7 @@ export default function BaseAuthenticatedPage(): React.ReactElement { const reloadUserInfo = async () => { try { loadingMessage.show("Refreshing user information..."); + await loadUserInfo(); } catch (e) { console.error(`Failed to load user information! ${e}`); alert(`Failed to load user information! ${e}`); From b10ec9ce922d4624309dee8682fe99e9273ccee2 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 11 Nov 2025 17:58:24 +0100 Subject: [PATCH 026/124] Cleanup code --- .../src/matrix_connection/matrix_client.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index c06f7a0..fa44e3d 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -61,10 +61,6 @@ enum MatrixClientError { DecodeStoredSession(serde_json::Error), #[error("Failed to restore stored session! {0}")] RestoreSession(matrix_sdk::Error), - #[error("Failed to disconnect user! {0}")] - DisconnectUser(anyhow::Error), - #[error("Failed to refresh access token! {0}")] - InitialRefreshToken(RefreshTokenError), #[error("Failed to parse auth redirect URL! {0}")] ParseAuthRedirectURL(url::ParseError), #[error("Failed to build auth request! {0}")] @@ -175,13 +171,9 @@ impl MatrixClient { log::warn!( "Refresh token rejected by server, token must have been invalidated! {refresh_error}" ); - // TODO : resolve - /*client - .disconnect() - .await - .map_err(MatrixClientError::DisconnectUser)?;*/ + } else { + log::warn!("Failed to refresh token! {refresh_error}"); } - //return Err(MatrixClientError::InitialRefreshToken(refresh_error).into()); } } From 8fdf1d57ebd792ed0ea2c7776e0995bc1d556130 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 11 Nov 2025 21:19:54 +0100 Subject: [PATCH 027/124] Create & list tokens --- matrixgw_backend/Cargo.lock | 24 +++ matrixgw_backend/Cargo.toml | 3 +- matrixgw_backend/src/constants.rs | 3 + matrixgw_backend/src/controllers/mod.rs | 1 + .../src/controllers/tokens_controller.rs | 36 +++++ .../src/extractors/auth_extractor.rs | 13 +- matrixgw_backend/src/main.rs | 7 +- matrixgw_backend/src/users.rs | 137 +++++++++++++++--- 8 files changed, 202 insertions(+), 22 deletions(-) create mode 100644 matrixgw_backend/src/controllers/tokens_controller.rs diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index 59988bb..33a12ff 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -2530,6 +2530,29 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" +[[package]] +name = "lazy-regex" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -3012,6 +3035,7 @@ dependencies = [ "hex", "ipnet", "jwt-simple", + "lazy-regex", "lazy_static", "light-openid", "log", diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index 26e819c..9474195 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -31,4 +31,5 @@ mailchecker = "6.0.19" matrix-sdk = "0.14.0" url = "2.5.7" ractor = "0.15.9" -serde_json = "1.0.145" \ No newline at end of file +serde_json = "1.0.145" +lazy-regex = "3.4.2" \ No newline at end of file diff --git a/matrixgw_backend/src/constants.rs b/matrixgw_backend/src/constants.rs index d46042c..2b8b86a 100644 --- a/matrixgw_backend/src/constants.rs +++ b/matrixgw_backend/src/constants.rs @@ -4,6 +4,9 @@ pub const API_AUTH_HEADER: &str = "x-client-auth"; /// Max token validity, in seconds pub const API_TOKEN_JWT_MAX_DURATION: u64 = 15 * 60; +/// Length of generated tokens +pub const TOKENS_LEN: usize = 50; + /// Session-specific constants pub mod sessions { /// OpenID auth session state key diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 8ddf218..0fbc422 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -5,6 +5,7 @@ use std::error::Error; pub mod auth_controller; pub mod matrix_link_controller; pub mod server_controller; +pub mod tokens_controller; #[derive(thiserror::Error, Debug)] pub enum HttpFailure { diff --git a/matrixgw_backend/src/controllers/tokens_controller.rs b/matrixgw_backend/src/controllers/tokens_controller.rs new file mode 100644 index 0000000..49350e3 --- /dev/null +++ b/matrixgw_backend/src/controllers/tokens_controller.rs @@ -0,0 +1,36 @@ +use crate::controllers::HttpResult; +use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; +use crate::users::{APIToken, BaseAPIToken}; +use actix_web::HttpResponse; + +/// Create a new token +pub async fn create(auth: AuthExtractor) -> HttpResult { + if matches!(auth.method, AuthenticatedMethod::Token(_)) { + return Ok(HttpResponse::Forbidden() + .json("It is not allowed to create a token using another token!")); + } + + let base = auth.decode_json_body::()?; + + if let Some(err) = base.check() { + return Ok(HttpResponse::BadRequest().json(err)); + } + + let token = APIToken::create(&auth.as_ref().email, base).await?; + + Ok(HttpResponse::Ok().json(token)) +} + +/// Get the list of tokens of current user +pub async fn get_list(auth: AuthExtractor) -> HttpResult { + Ok(HttpResponse::Ok().json( + APIToken::list_user(&auth.as_ref().email) + .await? + .into_iter() + .map(|mut t| { + t.secret = String::new(); + t + }) + .collect::>(), + )) +} diff --git a/matrixgw_backend/src/extractors/auth_extractor.rs b/matrixgw_backend/src/extractors/auth_extractor.rs index 277ffd6..b3a390f 100644 --- a/matrixgw_backend/src/extractors/auth_extractor.rs +++ b/matrixgw_backend/src/extractors/auth_extractor.rs @@ -34,6 +34,12 @@ pub struct AuthExtractor { pub payload: Option>, } +impl AsRef for AuthExtractor { + fn as_ref(&self) -> &User { + &self.user + } +} + impl AuthExtractor { pub fn decode_json_body(&self) -> anyhow::Result { let payload = self @@ -156,8 +162,9 @@ impl AuthExtractor { } // Check IP restriction - if let Some(net) = token.network - && !net.contains(&remote_ip) + if let Some(nets) = &token.base.networks + && !nets.is_empty() + && !nets.iter().any(|n| n.contains(&remote_ip)) { log::error!( "Trying to use token {:?} from unauthorized IP address: {remote_ip:?}", @@ -169,7 +176,7 @@ impl AuthExtractor { } // Check for write access - if token.read_only && !req.method().is_safe() { + if token.base.read_only && !req.method().is_safe() { return Err(actix_web::error::ErrorBadRequest( "Read only token cannot perform write operations!", )); diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index f0af96a..7004740 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -9,7 +9,9 @@ use actix_web::{App, HttpServer, web}; use matrixgw_backend::app_config::AppConfig; use matrixgw_backend::broadcast_messages::BroadcastMessage; use matrixgw_backend::constants; -use matrixgw_backend::controllers::{auth_controller, matrix_link_controller, server_controller}; +use matrixgw_backend::controllers::{ + auth_controller, matrix_link_controller, server_controller, tokens_controller, +}; use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor; use matrixgw_backend::users::User; use ractor::Actor; @@ -110,6 +112,9 @@ async fn main() -> std::io::Result<()> { "/api/matrix_link/set_recovery_key", web::post().to(matrix_link_controller::set_recovery_key), ) + // API Tokens controller + .route("/api/token", web::post().to(tokens_controller::create)) + .route("/api/tokens", web::get().to(tokens_controller::get_list)) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs index 3dd37da..1ac5253 100644 --- a/matrixgw_backend/src/users.rs +++ b/matrixgw_backend/src/users.rs @@ -1,6 +1,10 @@ use crate::app_config::AppConfig; +use crate::constants; +use crate::controllers::server_controller::ServerConstraints; use crate::matrix_connection::matrix_client::EncryptionRecoveryState; +use crate::utils::rand_utils::rand_string; use crate::utils::time_utils::time_secs; +use anyhow::Context; use jwt_simple::reexports::serde_json; use std::cmp::min; use std::str::FromStr; @@ -14,6 +18,8 @@ enum MatrixGWUserError { DecodeUserMetadata(serde_json::Error), #[error("Failed to save user metadata: {0}")] SaveUserMetadata(std::io::Error), + #[error("Failed to create API token directory: {0}")] + CreateApiTokensDirectory(std::io::Error), #[error("Failed to delete API token: {0}")] DeleteToken(std::io::Error), #[error("Failed to load API token: {0}")] @@ -101,17 +107,63 @@ impl User { } } -/// Single API client information +/// Base API token information #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] -pub struct APIToken { - /// Token unique ID - pub id: APITokenID, - - /// Client description - pub description: String, +pub struct BaseAPIToken { + /// Token name + pub name: String, /// Restricted API network for token - pub network: Option, + pub networks: Option>, + + /// Read only access + pub read_only: bool, + + /// Token max inactivity + pub max_inactivity: u32, + + /// Token expiration + pub expiration: Option, +} + +impl BaseAPIToken { + /// Check API token information validity + pub fn check(&self) -> Option<&'static str> { + let constraints = ServerConstraints::default(); + + if !lazy_regex::regex!("^[a-zA-Z0-9 :-]+$").is_match(&self.name) { + return Some("Token name contains invalid characters!"); + } + + if !constraints.token_name.check_str(&self.name) { + return Some("Invalid token name length!"); + } + + if !constraints + .token_max_inactivity + .check_u32(self.max_inactivity) + { + return Some("Invalid token max inactivity!"); + } + + if let Some(expiration) = self.expiration + && expiration <= time_secs() + { + return Some("Given expiration time is in the past!"); + } + + None + } +} + +/// Single API token information +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct APIToken { + #[serde(flatten)] + pub base: BaseAPIToken, + + /// Token unique ID + pub id: APITokenID, /// Client secret pub secret: String, @@ -121,15 +173,58 @@ pub struct APIToken { /// Client last usage time pub last_used: u64, - - /// Read only access - pub read_only: bool, - - /// Token max inactivity - pub max_inactivity: u64, } impl APIToken { + /// Get the list of tokens of a user + pub async fn list_user(email: &UserEmail) -> anyhow::Result> { + let tokens_dir = AppConfig::get().user_api_token_directory(email); + + if !tokens_dir.exists() { + return Ok(vec![]); + } + + let mut list = vec![]; + for u in std::fs::read_dir(&tokens_dir)? { + let entry = u?; + list.push( + Self::load( + email, + &APITokenID::from_str( + entry + .file_name() + .to_str() + .context("Cannot decode API Token ID as string!")?, + )?, + ) + .await?, + ); + } + Ok(list) + } + + /// Create a new token + pub async fn create(email: &UserEmail, base: BaseAPIToken) -> anyhow::Result { + let tokens_dir = AppConfig::get().user_api_token_directory(email); + + if !tokens_dir.exists() { + std::fs::create_dir_all(tokens_dir) + .map_err(MatrixGWUserError::CreateApiTokensDirectory)?; + } + + let token = APIToken { + base, + id: Default::default(), + secret: rand_string(constants::TOKENS_LEN), + created: time_secs(), + last_used: time_secs(), + }; + + token.write(email).await?; + + Ok(token) + } + /// Get a token information pub async fn load(email: &UserEmail, id: &APITokenID) -> anyhow::Result { let token_file = AppConfig::get().user_api_token_metadata_file(email, id); @@ -158,13 +253,21 @@ impl APIToken { } pub fn shall_update_time_used(&self) -> bool { - let refresh_interval = min(600, self.max_inactivity / 10); + let refresh_interval = min(600, self.base.max_inactivity / 10); - (self.last_used) < time_secs() - refresh_interval + (self.last_used) < time_secs() - refresh_interval as u64 } pub fn is_expired(&self) -> bool { - (self.last_used + self.max_inactivity) < time_secs() + // Check for hard coded expiration + if let Some(exp_time) = self.base.expiration + && exp_time < time_secs() + { + return true; + } + + // Control max token inactivity + (self.last_used + self.base.max_inactivity as u64) < time_secs() } } From 7c78eb541efa6a5e552d64cef5a6b8adc849bd07 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 11 Nov 2025 21:24:19 +0100 Subject: [PATCH 028/124] Fix example API client --- matrixgw_backend/examples/api_curl.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/matrixgw_backend/examples/api_curl.rs b/matrixgw_backend/examples/api_curl.rs index 3b8b7ff..f776dc8 100644 --- a/matrixgw_backend/examples/api_curl.rs +++ b/matrixgw_backend/examples/api_curl.rs @@ -2,11 +2,13 @@ use clap::Parser; use jwt_simple::algorithms::HS256Key; use jwt_simple::prelude::{Clock, Duration, JWTClaims, MACLike}; use matrixgw_backend::constants; -use matrixgw_backend::extractors::auth_extractor::TokenClaims; +use matrixgw_backend::extractors::auth_extractor::{MatrixJWTKID, TokenClaims}; +use matrixgw_backend::users::{APITokenID, UserEmail}; use matrixgw_backend::utils::rand_utils::rand_string; use std::ops::Add; use std::os::unix::prelude::CommandExt; use std::process::Command; +use std::str::FromStr; /// cURL wrapper to query MatrixGW #[derive(Parser, Debug)] @@ -20,9 +22,9 @@ struct Args { #[arg(short('i'), long, env)] token_id: String, - /// User ID + /// User email #[arg(short('u'), long, env)] - user_id: String, + user_mail: String, /// Token secret #[arg(short('t'), long, env)] @@ -69,11 +71,14 @@ fn main() { }; let jwt = key - .with_key_id(&format!( - "{}#{}", - urlencoding::encode(&args.user_id), - urlencoding::encode(&args.token_id) - )) + .with_key_id( + &MatrixJWTKID { + user_email: UserEmail(args.user_mail), + id: APITokenID::from_str(args.token_id.as_str()) + .expect("Failed to decode token ID!"), + } + .to_string(), + ) .authenticate(claims) .expect("Failed to sign JWT!"); From 5ca126eef74ea8d86c903f66993374d6e65d160e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 12 Nov 2025 08:06:47 +0100 Subject: [PATCH 029/124] Split recovery key dialog in new file --- .../src/dialogs/SetRecoveryKeyDialog.tsx | 73 +++++++++++++++++++ .../src/routes/MatrixLinkRoute.tsx | 63 ++-------------- 2 files changed, 81 insertions(+), 55 deletions(-) create mode 100644 matrixgw_frontend/src/dialogs/SetRecoveryKeyDialog.tsx diff --git a/matrixgw_frontend/src/dialogs/SetRecoveryKeyDialog.tsx b/matrixgw_frontend/src/dialogs/SetRecoveryKeyDialog.tsx new file mode 100644 index 0000000..22c0aa6 --- /dev/null +++ b/matrixgw_frontend/src/dialogs/SetRecoveryKeyDialog.tsx @@ -0,0 +1,73 @@ +import { + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + TextField, + DialogActions, + Button, +} from "@mui/material"; +import { MatrixLinkApi } from "../api/MatrixLinkApi"; +import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider"; +import { useLoadingMessage } from "../hooks/contexts_provider/LoadingMessageProvider"; +import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider"; +import React from "react"; +import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; + +export function SetRecoveryKeyDialog(p: { + open: boolean; + onClose: () => void; +}): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + const loadingMessage = useLoadingMessage(); + + const user = useUserInfo(); + + const [newKey, setNewKey] = React.useState(""); + + const handleSubmitKey = async () => { + try { + loadingMessage.show("Updating recovery key..."); + + await MatrixLinkApi.SetRecoveryKey(newKey); + setNewKey(""); + p.onClose(); + + snackbar("Recovery key successfully updated!"); + user.reloadUserInfo(); + } catch (e) { + console.error(`Failed to set new recovery key! ${e}`); + alert(`Failed to set new recovery key! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + + return ( + + Set new recovery key + + + Enter below you recovery key to verify this session and gain access to + old messages. + + setNewKey(e.target.value)} + fullWidth + /> + + + + + + + ); +} diff --git a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx index 2d16bc6..1bb675d 100644 --- a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx +++ b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx @@ -8,16 +8,11 @@ import { Card, CardActions, CardContent, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - TextField, Typography, } from "@mui/material"; import React from "react"; import { MatrixLinkApi } from "../api/MatrixLinkApi"; +import { SetRecoveryKeyDialog } from "../dialogs/SetRecoveryKeyDialog"; import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider"; import { useConfirm } from "../hooks/contexts_provider/ConfirmDialogProvider"; import { useLoadingMessage } from "../hooks/contexts_provider/LoadingMessageProvider"; @@ -152,34 +147,12 @@ function ConnectedCard(): React.ReactElement { } function EncryptionKeyStatus(): React.ReactElement { - const alert = useAlert(); - const snackbar = useSnackbar(); - const loadingMessage = useLoadingMessage(); - const user = useUserInfo(); - const [typeNewKey, setTypeNewKey] = React.useState(false); - const [newKey, setNewKey] = React.useState(""); + const [openSetKeyDialog, setOpenSetKeyDialog] = React.useState(false); - const handleSetKey = () => setTypeNewKey(true); - const cancelSetKey = () => setTypeNewKey(false); - const handleSubmitKey = async () => { - try { - loadingMessage.show("Updating recovery key..."); - - await MatrixLinkApi.SetRecoveryKey(newKey); - setNewKey(""); - setTypeNewKey(false); - - snackbar("Recovery key successfully updated!"); - user.reloadUserInfo(); - } catch (e) { - console.error(`Failed to set new recovery key! ${e}`); - alert(`Failed to set new recovery key! ${e}`); - } finally { - loadingMessage.hide(); - } - }; + const handleSetKey = () => setOpenSetKeyDialog(true); + const handleCloseSetKey = () => setOpenSetKeyDialog(false); return ( <> @@ -222,30 +195,10 @@ function EncryptionKeyStatus(): React.ReactElement {
    {/* Set new key dialog */} - - Set new recovery key - - - Enter below you recovery key to verify this session and gain access - to old messages. - - setNewKey(e.target.value)} - fullWidth - /> - - - - - - + ); } From 3b7b368e13a049275160bcba43001369dc420f9e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 12 Nov 2025 08:14:16 +0100 Subject: [PATCH 030/124] Attempt to fix session restoration issues --- .../src/matrix_connection/matrix_client.rs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index fa44e3d..e2b636b 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -130,14 +130,14 @@ impl MatrixClient { }; // Check metadata - let oauth = client.client.oauth(); - let server_metadata = oauth - .server_metadata() - .await - .map_err(MatrixClientError::FetchServerMetadata)?; - log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer); - - if is_restoring { + if !is_restoring { + let oauth = client.client.oauth(); + let server_metadata = oauth + .server_metadata() + .await + .map_err(MatrixClientError::FetchServerMetadata)?; + log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer); + } else { let session: StoredSession = serde_json::from_str( std::fs::read_to_string(session_file_path) .map_err(MatrixClientError::LoadStoredSession)? @@ -292,20 +292,15 @@ impl MatrixClient { async fn save_stored_session(&self) -> anyhow::Result<()> { log::debug!("Save the stored session for {:?}...", self.email); - let user_session: UserSession = self + let full_session = self .client .oauth() - .user_session() + .full_session() .context("A logged in client must have a session")?; let stored_session = StoredSession { - user_session, - client_id: self - .client - .oauth() - .client_id() - .context("Client ID should be set at this point!")? - .clone(), + user_session: full_session.user, + client_id: full_session.client_id, }; let serialized_session = serde_json::to_string(&stored_session)?; From c8a48488fc30a8b9a22bdeec1d6c9595b94b856c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 13 Nov 2025 18:38:07 +0100 Subject: [PATCH 031/124] Fix session disconnection issue by removing automatic refresh on client initialization --- .../src/matrix_connection/matrix_client.rs | 20 ++----------------- matrixgw_backend/src/users.rs | 6 +++--- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index e2b636b..b147ad7 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -3,15 +3,13 @@ use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use crate::users::UserEmail; use crate::utils::rand_utils::rand_string; use anyhow::Context; -use matrix_sdk::authentication::oauth::error::{ - BasicErrorResponseType, OAuthDiscoveryError, RequestTokenError, -}; +use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError; use matrix_sdk::authentication::oauth::{ ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession, }; use matrix_sdk::encryption::recovery::RecoveryState; use matrix_sdk::ruma::serde::Raw; -use matrix_sdk::{Client, ClientBuildError, RefreshTokenError}; +use matrix_sdk::{Client, ClientBuildError}; use ractor::ActorRef; use serde::{Deserialize, Serialize}; use url::Url; @@ -161,20 +159,6 @@ impl MatrixClient { .encryption() .wait_for_e2ee_initialization_tasks() .await; - - // Force token refresh to make sure session is still alive, otherwise disconnect user - if let Err(refresh_error) = client.client.oauth().refresh_access_token().await { - if let RefreshTokenError::OAuth(e) = &refresh_error - && let OAuthError::RefreshToken(RequestTokenError::ServerResponse(e)) = &**e - && e.error() == &BasicErrorResponseType::InvalidGrant - { - log::warn!( - "Refresh token rejected by server, token must have been invalidated! {refresh_error}" - ); - } else { - log::warn!("Failed to refresh token! {refresh_error}"); - } - } } // Automatically save session when token gets refreshed diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs index 1ac5253..ad1e410 100644 --- a/matrixgw_backend/src/users.rs +++ b/matrixgw_backend/src/users.rs @@ -116,14 +116,14 @@ pub struct BaseAPIToken { /// Restricted API network for token pub networks: Option>, - /// Read only access - pub read_only: bool, - /// Token max inactivity pub max_inactivity: u32, /// Token expiration pub expiration: Option, + + /// Read only access + pub read_only: bool, } impl BaseAPIToken { From 72aaf7b082873261b267a438d7ba414867542c46 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 13 Nov 2025 21:03:38 +0100 Subject: [PATCH 032/124] Add token creation dialog --- matrixgw_frontend/package-lock.json | 158 +++++++++++++++++ matrixgw_frontend/package.json | 4 + matrixgw_frontend/src/App.tsx | 4 +- matrixgw_frontend/src/api/TokensApi.ts | 46 +++++ .../src/dialogs/CreateTokenDialog.tsx | 159 ++++++++++++++++++ matrixgw_frontend/src/main.tsx | 52 +++--- .../src/routes/APITokensRoute.tsx | 98 +++++++++++ matrixgw_frontend/src/utils/DateUtils.ts | 8 + matrixgw_frontend/src/utils/FormUtils.ts | 52 ++++++ .../src/widgets/CopyTextChip.tsx | 29 ++++ .../src/widgets/forms/CheckboxInput.tsx | 23 +++ .../src/widgets/forms/DateInput.tsx | 49 ++++++ .../src/widgets/forms/NetworksInput.tsx | 26 +++ .../src/widgets/forms/TextInput.tsx | 65 +++++++ 14 files changed, 748 insertions(+), 25 deletions(-) create mode 100644 matrixgw_frontend/src/api/TokensApi.ts create mode 100644 matrixgw_frontend/src/dialogs/CreateTokenDialog.tsx create mode 100644 matrixgw_frontend/src/routes/APITokensRoute.tsx create mode 100644 matrixgw_frontend/src/utils/DateUtils.ts create mode 100644 matrixgw_frontend/src/utils/FormUtils.ts create mode 100644 matrixgw_frontend/src/widgets/CopyTextChip.tsx create mode 100644 matrixgw_frontend/src/widgets/forms/CheckboxInput.tsx create mode 100644 matrixgw_frontend/src/widgets/forms/DateInput.tsx create mode 100644 matrixgw_frontend/src/widgets/forms/NetworksInput.tsx create mode 100644 matrixgw_frontend/src/widgets/forms/TextInput.tsx diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index 152f32f..9caabde 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -15,6 +15,10 @@ "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", + "@mui/x-date-pickers": "^8.17.0", + "dayjs": "^1.11.19", + "is-cidr": "^6.0.1", + "qrcode.react": "^4.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router": "^7.9.5" @@ -1039,6 +1043,94 @@ } } }, + "node_modules/@mui/x-date-pickers": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.17.0.tgz", + "integrity": "sha512-mrrkTJ1+r6MsPnKH/N5lCNJHkP0dZc2Fvd8fp5tyxa0jRyzwbxJKsadXooccoJWp65Z2vUjUuctXYUmubYP/Sg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "@mui/x-internals": "8.17.0", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.17.0.tgz", + "integrity": "sha512-KvmR0PPX1j2i44y0DXwzs45jIPMu/YZYXYy7xvzo+ZNdYebbW5LbVeG4zdEUnKHyOG02oHdI7MM9AxcZE16TBw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", @@ -1987,6 +2079,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/cidr-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-5.0.1.tgz", + "integrity": "sha512-2Apfc6qH9uwF3QHmlYBA8ExB9VHq+1/Doj9sEMY55TVBcpQ3y/+gmMpcNIBBtfb5k54Vphmta+1IxjMqPlWWAA==", + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "5.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2085,6 +2189,12 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2615,12 +2725,36 @@ "node": ">=0.8.19" } }, + "node_modules/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-cidr": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-6.0.1.tgz", + "integrity": "sha512-JIJlvXodfsoWFAvvjB7Elqu8qQcys2SZjkIJCLdk4XherUqZ6+zH7WIpXkp4B3ZxMH0Fz7zIsZwyvs6JfM0csw==", + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "5.0.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -3368,6 +3502,15 @@ "node": ">=6" } }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3464,6 +3607,12 @@ "react-dom": ">=16.6.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -3858,6 +4007,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "name": "rolldown-vite", "version": "7.1.14", diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index 823193f..99ed122 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -17,6 +17,10 @@ "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", + "@mui/x-date-pickers": "^8.17.0", + "dayjs": "^1.11.19", + "is-cidr": "^6.0.1", + "qrcode.react": "^4.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router": "^7.9.5" diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index 7cd3001..a202bcb 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -7,14 +7,15 @@ import { } from "react-router"; import { AuthApi } from "./api/AuthApi"; import { ServerApi } from "./api/ServerApi"; +import { APITokensRoute } from "./routes/APITokensRoute"; import { LoginRoute } from "./routes/auth/LoginRoute"; import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; import { HomeRoute } from "./routes/HomeRoute"; +import { MatrixAuthCallback } from "./routes/MatrixAuthCallback"; import { MatrixLinkRoute } from "./routes/MatrixLinkRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute"; import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage"; -import { MatrixAuthCallback } from "./routes/MatrixAuthCallback"; interface AuthContext { signedIn: boolean; @@ -41,6 +42,7 @@ export function App(): React.ReactElement { } /> } /> } /> + } /> } /> ) : ( diff --git a/matrixgw_frontend/src/api/TokensApi.ts b/matrixgw_frontend/src/api/TokensApi.ts new file mode 100644 index 0000000..268a2ae --- /dev/null +++ b/matrixgw_frontend/src/api/TokensApi.ts @@ -0,0 +1,46 @@ +import { APIClient } from "./ApiClient"; + +export interface BaseToken { + name: string; + networks?: string[]; + max_inactivity: number; + expiration?: number; + read_only: boolean; +} + +export interface Token extends BaseToken { + id: number; + created: number; + last_used: number; +} + +export interface TokenWithSecret extends Token { + secret: string; +} + +export class TokensApi { + /** + * Get the list of tokens of the current user + */ + static async GetList(): Promise { + return ( + await APIClient.exec({ + uri: "/tokens", + method: "GET", + }) + ).data; + } + + /** + * Create a new token + */ + static async Create(t: BaseToken): Promise { + return ( + await APIClient.exec({ + uri: "/token", + method: "POST", + jsonData: t, + }) + ).data; + } +} diff --git a/matrixgw_frontend/src/dialogs/CreateTokenDialog.tsx b/matrixgw_frontend/src/dialogs/CreateTokenDialog.tsx new file mode 100644 index 0000000..077e174 --- /dev/null +++ b/matrixgw_frontend/src/dialogs/CreateTokenDialog.tsx @@ -0,0 +1,159 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from "@mui/material"; +import React from "react"; +import { ServerApi } from "../api/ServerApi"; +import { + TokensApi, + type BaseToken, + type TokenWithSecret, +} from "../api/TokensApi"; +import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider"; +import { useLoadingMessage } from "../hooks/contexts_provider/LoadingMessageProvider"; +import { time } from "../utils/DateUtils"; +import { + checkConstraint, + checkNumberConstraint, + isIPNetworkValid, +} from "../utils/FormUtils"; +import { CheckboxInput } from "../widgets/forms/CheckboxInput"; +import { DateInput } from "../widgets/forms/DateInput"; +import { NetworksInput } from "../widgets/forms/NetworksInput"; +import { TextInput } from "../widgets/forms/TextInput"; + +const SECS_IN_DAY = 3600 * 24; + +export function CreateTokenDialog(p: { + open: boolean; + onClose: () => void; + onCreated: (t: TokenWithSecret) => void; +}): React.ReactElement { + const alert = useAlert(); + const loadingMessage = useLoadingMessage(); + + const [newTokenUndef, setNewToken] = React.useState(); + const newToken: BaseToken = newTokenUndef ?? { + name: "", + max_inactivity: 3600 * 24 * 90, + read_only: false, + }; + + const valid = + checkConstraint(ServerApi.Config.constraints.token_name, newToken.name) === + undefined && + checkNumberConstraint( + ServerApi.Config.constraints.token_max_inactivity, + newToken.max_inactivity + ) === undefined && + (newToken.networks === undefined || + newToken.networks.every((n) => isIPNetworkValid(n))); + + const handleSubmit = async () => { + try { + loadingMessage.show("Creating access token..."); + const token = await TokensApi.Create(newToken); + p.onCreated(token); + + // Clear form + setNewToken(undefined); + } catch (e) { + console.error(`Failed to create token! ${e}`); + alert(`Failed to create API token! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + + return ( + + Create new API token + + { + setNewToken({ + ...newToken, + name: v ?? "", + }); + }} + size={ServerApi.Config.constraints.token_name} + /> + + { + setNewToken({ + ...newToken, + networks: v, + }); + }} + /> + + { + setNewToken({ + ...newToken, + max_inactivity: Number(i) * SECS_IN_DAY, + }); + }} + size={{ + min: + ServerApi.Config.constraints.token_max_inactivity.min / + SECS_IN_DAY, + max: + ServerApi.Config.constraints.token_max_inactivity.max / + SECS_IN_DAY, + }} + /> + + { + setNewToken((t) => { + return { + ...(t ?? newToken), + expiration: i ?? undefined, + }; + }); + }} + disablePast + checkValue={(s) => s > time()} + /> + + { + setNewToken({ + ...newToken, + read_only: v, + }); + }} + /> + + + + + + + ); +} diff --git a/matrixgw_frontend/src/main.tsx b/matrixgw_frontend/src/main.tsx index afd73ad..6508bc0 100644 --- a/matrixgw_frontend/src/main.tsx +++ b/matrixgw_frontend/src/main.tsx @@ -3,39 +3,43 @@ import "@fontsource/roboto/400.css"; import "@fontsource/roboto/500.css"; import "@fontsource/roboto/700.css"; +import { CssBaseline } from "@mui/material"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; -import "./index.css"; +import { ServerApi } from "./api/ServerApi"; import { App } from "./App"; import { AlertDialogProvider } from "./hooks/contexts_provider/AlertDialogProvider"; import { ConfirmDialogProvider } from "./hooks/contexts_provider/ConfirmDialogProvider"; -import { SnackbarProvider } from "./hooks/contexts_provider/SnackbarProvider"; import { LoadingMessageProvider } from "./hooks/contexts_provider/LoadingMessageProvider"; -import { AsyncWidget } from "./widgets/AsyncWidget"; -import { ServerApi } from "./api/ServerApi"; +import { SnackbarProvider } from "./hooks/contexts_provider/SnackbarProvider"; +import "./index.css"; import { AppTheme } from "./theme/AppTheme"; -import { CssBaseline } from "@mui/material"; +import { AsyncWidget } from "./widgets/AsyncWidget"; createRoot(document.getElementById("root")!).render( - - - - - - - { - await ServerApi.LoadConfig(); - }} - errMsg="Failed to load static server configuration!" - build={() => } - /> - - - - - + + + + + + + + { + await ServerApi.LoadConfig(); + }} + errMsg="Failed to load static server configuration!" + build={() => } + /> + + + + + + ); diff --git a/matrixgw_frontend/src/routes/APITokensRoute.tsx b/matrixgw_frontend/src/routes/APITokensRoute.tsx new file mode 100644 index 0000000..5225354 --- /dev/null +++ b/matrixgw_frontend/src/routes/APITokensRoute.tsx @@ -0,0 +1,98 @@ +import AddIcon from "@mui/icons-material/Add"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { Alert, AlertTitle, IconButton, Tooltip } from "@mui/material"; +import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; +import React from "react"; +import { CreateTokenDialog } from "../dialogs/CreateTokenDialog"; +import { type TokenWithSecret } from "../api/TokensApi"; +import { APIClient } from "../api/ApiClient"; +import { QRCodeCanvas } from "qrcode.react"; +import { CopyTextChip } from "../widgets/CopyTextChip"; + +export function APITokensRoute(): React.ReactElement { + const [openCreateTokenDialog, setOpenCreateTokenDialog] = + React.useState(false); + + const [createdToken, setCreatedToken] = + React.useState(null); + + const handleRefreshTokensList = () => { + //throw new Error("todo"); + }; + + const handleOpenCreateTokenDialog = () => setOpenCreateTokenDialog(true); + + const handleCancelCreateToken = () => setOpenCreateTokenDialog(false); + + const handleCreatedToken = (s: TokenWithSecret) => { + setCreatedToken(s); + setOpenCreateTokenDialog(false); + handleRefreshTokensList(); + }; + + return ( + + + + + + +    + + + + + + + } + > + + {createdToken && } +

    TODO list

    +
    + ); +} + +function CreatedToken(p: { token: TokenWithSecret }): React.ReactElement { + return ( + +
    +
    +
    + +
    +
    + Mobile App Qr Code +
    +
    + Token successfully created + The API token {p.token.name} was successfully created. Please + note the following information as they won't be available after. +
    +
    + API URL: +
    + Token ID: +
    + Token secret: +
    +
    +
    + ); +} diff --git a/matrixgw_frontend/src/utils/DateUtils.ts b/matrixgw_frontend/src/utils/DateUtils.ts new file mode 100644 index 0000000..c44c16f --- /dev/null +++ b/matrixgw_frontend/src/utils/DateUtils.ts @@ -0,0 +1,8 @@ +/** + * Get UNIX time + * + * @returns Number of seconds since Epoch + */ +export function time(): number { + return Math.floor(new Date().getTime() / 1000); +} diff --git a/matrixgw_frontend/src/utils/FormUtils.ts b/matrixgw_frontend/src/utils/FormUtils.ts new file mode 100644 index 0000000..e47e9ec --- /dev/null +++ b/matrixgw_frontend/src/utils/FormUtils.ts @@ -0,0 +1,52 @@ +import isCidr from "is-cidr"; +import type { LenConstraint } from "../api/ServerApi"; + +/** + * Check if a constraint was respected or not + * + * @returns An error message appropriate for the constraint + * violation, if any, or undefined otherwise + */ +export function checkConstraint( + constraint: LenConstraint, + value: string | undefined +): string | undefined { + value = value ?? ""; + if (value.length < constraint.min) + return `Please specify at least ${constraint.min} characters!`; + + if (value.length > constraint.max) + return `Please specify at least ${constraint.min} characters!`; + + return undefined; +} + +/** + * Check if a number constraint was respected or not + * + * @returns An error message appropriate for the constraint + * violation, if any, or undefined otherwise + */ +export function checkNumberConstraint( + constraint: LenConstraint, + value: number +): string | undefined { + value = value ?? ""; + if (value < constraint.min) + return `Value is below accepted minimum (${constraint.min})!`; + + if (value > constraint.max) + return `Value is above accepted maximum (${constraint.min})!`; + + return undefined; +} + +/** + * Check whether a given IP network address is valid or not + * + * @param ip The IP network to check + * @returns true if the address is valid, false otherwise + */ +export function isIPNetworkValid(ip: string): boolean { + return isCidr(ip) !== 0; +} diff --git a/matrixgw_frontend/src/widgets/CopyTextChip.tsx b/matrixgw_frontend/src/widgets/CopyTextChip.tsx new file mode 100644 index 0000000..864ef58 --- /dev/null +++ b/matrixgw_frontend/src/widgets/CopyTextChip.tsx @@ -0,0 +1,29 @@ +import { Chip, Tooltip } from "@mui/material"; +import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider"; +import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider"; + +export function CopyTextChip(p: { text: string }): React.ReactElement { + const snackbar = useSnackbar(); + const alert = useAlert(); + + const copyTextToClipboard = () => { + try { + navigator.clipboard.writeText(p.text); + snackbar(`'${p.text}' was copied to clipboard.`); + } catch (e) { + console.error(`Failed to copy text to the clipboard! ${e}`); + alert(p.text); + } + }; + + return ( + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/forms/CheckboxInput.tsx b/matrixgw_frontend/src/widgets/forms/CheckboxInput.tsx new file mode 100644 index 0000000..dcf7c71 --- /dev/null +++ b/matrixgw_frontend/src/widgets/forms/CheckboxInput.tsx @@ -0,0 +1,23 @@ +import { Checkbox, FormControlLabel } from "@mui/material"; + +export function CheckboxInput(p: { + editable: boolean; + label: string; + checked: boolean | undefined; + onValueChange: (v: boolean) => void; +}): React.ReactElement { + return ( + { + p.onValueChange(e.target.checked); + }} + /> + } + label={p.label} + /> + ); +} diff --git a/matrixgw_frontend/src/widgets/forms/DateInput.tsx b/matrixgw_frontend/src/widgets/forms/DateInput.tsx new file mode 100644 index 0000000..c186e14 --- /dev/null +++ b/matrixgw_frontend/src/widgets/forms/DateInput.tsx @@ -0,0 +1,49 @@ +import { DateField } from "@mui/x-date-pickers"; +import dayjs from "dayjs"; +import { TextInput } from "./TextInput"; + +export function DateInput(p: { + editable?: boolean; + required?: boolean; + label: string; + value: number | undefined | null; + checkValue?: (s: number) => boolean; + disableFuture?: boolean; + disablePast?: boolean; + onChange: (newVal: number | undefined | null) => void; +}): React.ReactElement { + const date = p.value ? dayjs.unix(p.value) : undefined; + + const error = p.value && p.checkValue && !p.checkValue(p.value); + + if (!p.editable) + return ( + + ); + + return ( + p.onChange(v?.unix())} + slotProps={{ + textField: { + fullWidth: true, + label: p.label, + variant: "standard", + }, + inputAdornment: { + variant: "standard", + }, + }} + disableFuture={p.disableFuture} + disablePast={p.disablePast} + error={error === true} + format="DD/MM/YYYY" + /> + ); +} diff --git a/matrixgw_frontend/src/widgets/forms/NetworksInput.tsx b/matrixgw_frontend/src/widgets/forms/NetworksInput.tsx new file mode 100644 index 0000000..a583196 --- /dev/null +++ b/matrixgw_frontend/src/widgets/forms/NetworksInput.tsx @@ -0,0 +1,26 @@ +import { isIPNetworkValid } from "../../utils/FormUtils"; +import { TextInput } from "./TextInput"; + +function rebuildNetworksList(val?: string): string[] | undefined { + if (!val || val.trim() === "") return undefined; + + return val.split(",").map((v) => v.trim()); +} + +export function NetworksInput(p: { + editable?: boolean; + label: string; + value?: string[]; + onChange: (n: string[] | undefined) => void; +}): React.ReactElement { + const textValue = (p.value ?? []).join(", ").trim(); + return ( + p.onChange(rebuildNetworksList(i))} + checkValue={(v) => (rebuildNetworksList(v) ?? []).every(isIPNetworkValid)} + /> + ); +} diff --git a/matrixgw_frontend/src/widgets/forms/TextInput.tsx b/matrixgw_frontend/src/widgets/forms/TextInput.tsx new file mode 100644 index 0000000..8b461a8 --- /dev/null +++ b/matrixgw_frontend/src/widgets/forms/TextInput.tsx @@ -0,0 +1,65 @@ +import { TextField, type TextFieldVariants } from "@mui/material"; +import type { LenConstraint } from "../../api/ServerApi"; + +/** + * Text input + */ +export function TextInput(p: { + label?: string; + editable?: boolean; + required?: boolean; + value?: string; + onValueChange?: (newVal: string | undefined) => void; + size?: LenConstraint; + checkValue?: (s: string) => boolean; + multiline?: boolean; + minRows?: number; + maxRows?: number; + placeholder?: string; + type?: React.HTMLInputTypeAttribute; + style?: React.CSSProperties; + helperText?: string; + variant?: TextFieldVariants; +}): React.ReactElement { + if (!p.editable && (p.value ?? "") === "") return <>; + + let valueError = undefined; + if (p.value && p.value.length > 0) { + if (p.size?.min && p.type !== "number" && p.value.length < p.size.min) + valueError = `Please specify at least ${p.size.min} characters !`; + if (p.checkValue && !p.checkValue(p.value)) valueError = "Invalid value!"; + if ( + p.type === "number" && + p.size && + (Number(p.value) > p.size.max || Number(p.value) < p.size.min) + ) + valueError = "Invalid size range!"; + } + + return ( + + p.onValueChange?.( + e.target.value.length === 0 ? undefined : e.target.value + ) + } + slotProps={{ + input: { + readOnly: !p.editable, + type: p.type, + }, + htmlInput: { maxLength: p.size?.max, placeholder: p.placeholder }, + }} + variant={p.variant ?? "standard"} + style={p.style ?? { width: "100%", marginBottom: "15px" }} + multiline={p.multiline} + minRows={p.minRows} + maxRows={p.maxRows} + error={valueError !== undefined} + helperText={valueError ?? p.helperText} + /> + ); +} From 26832680424d80c3fe65bcffbc1d267377d8464e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 13 Nov 2025 21:16:45 +0100 Subject: [PATCH 033/124] Load the list of API tokens --- .../src/routes/APITokensRoute.tsx | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/matrixgw_frontend/src/routes/APITokensRoute.tsx b/matrixgw_frontend/src/routes/APITokensRoute.tsx index 5225354..f6c4470 100644 --- a/matrixgw_frontend/src/routes/APITokensRoute.tsx +++ b/matrixgw_frontend/src/routes/APITokensRoute.tsx @@ -1,23 +1,33 @@ import AddIcon from "@mui/icons-material/Add"; import RefreshIcon from "@mui/icons-material/Refresh"; import { Alert, AlertTitle, IconButton, Tooltip } from "@mui/material"; -import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; -import React from "react"; -import { CreateTokenDialog } from "../dialogs/CreateTokenDialog"; -import { type TokenWithSecret } from "../api/TokensApi"; -import { APIClient } from "../api/ApiClient"; import { QRCodeCanvas } from "qrcode.react"; +import React from "react"; +import { APIClient } from "../api/ApiClient"; +import { TokensApi, type Token, type TokenWithSecret } from "../api/TokensApi"; +import { CreateTokenDialog } from "../dialogs/CreateTokenDialog"; +import { AsyncWidget } from "../widgets/AsyncWidget"; import { CopyTextChip } from "../widgets/CopyTextChip"; +import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; export function APITokensRoute(): React.ReactElement { + const count = React.useRef(0); + const [openCreateTokenDialog, setOpenCreateTokenDialog] = React.useState(false); const [createdToken, setCreatedToken] = React.useState(null); + const [list, setList] = React.useState(); + + const load = async () => { + setList(await TokensApi.GetList()); + }; + const handleRefreshTokensList = () => { - //throw new Error("todo"); + count.current += 1; + setList(undefined); }; const handleOpenCreateTokenDialog = () => setOpenCreateTokenDialog(true); @@ -49,13 +59,24 @@ export function APITokensRoute(): React.ReactElement { } > + {/* Create token dialog anchor */} + + {/* Info about created token */} {createdToken && } -

    TODO list

    + + {/* Tokens list */} + <>{list?.length} tokens} + /> ); } From 02e55758922546ee994365918c3218520200fba6 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 14 Nov 2025 09:07:22 +0100 Subject: [PATCH 034/124] Display the list of API tokens --- matrixgw_frontend/package-lock.json | 115 ++++++++++++++++++ matrixgw_frontend/package.json | 2 + .../src/routes/APITokensRoute.tsx | 96 ++++++++++++++- matrixgw_frontend/src/widgets/TimeWidget.tsx | 86 +++++++++++++ 4 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 matrixgw_frontend/src/widgets/TimeWidget.tsx diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index 9caabde..cf89c4a 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -15,7 +15,9 @@ "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", + "@mui/x-data-grid": "^8.18.0", "@mui/x-date-pickers": "^8.17.0", + "date-and-time": "^4.1.0", "dayjs": "^1.11.19", "is-cidr": "^6.0.1", "qrcode.react": "^4.2.0", @@ -1043,6 +1045,66 @@ } } }, + "node_modules/@mui/x-data-grid": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.18.0.tgz", + "integrity": "sha512-g8y5EI3TNqrimHpH/Hv6u6i04cbvsqh39Tg4bZEhGq+SDxWp42iABlUvB7p+gtXfyd+IbmpfzUQ1hOCsHlTMZw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "@mui/x-internals": "8.18.0", + "@mui/x-virtualizer": "0.2.8", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.18.0.tgz", + "integrity": "sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/x-date-pickers": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.17.0.tgz", @@ -1131,6 +1193,50 @@ "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/@mui/x-virtualizer": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.2.8.tgz", + "integrity": "sha512-hCkhTg3BLLbf0SIw9Cx/NHTCUmbna+P5F2V+Bcv/9XiYhfzzmhYnm68+V6vOOhKVbV3j8JKsUEqcTC9K2Jpu8A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "@mui/x-internals": "8.18.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@mui/x-virtualizer/node_modules/@mui/x-internals": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.18.0.tgz", + "integrity": "sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", @@ -2189,6 +2295,15 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/date-and-time": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-4.1.0.tgz", + "integrity": "sha512-tFdrmBPZrR7bun6jqmlEy/dsjV2JLeUdGALfbKdB7mf0ItMNkYYklxjFE0voGg5oapIaE7WctMClkuRzyU9pig==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/dayjs": { "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index 99ed122..f9b7daf 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -17,7 +17,9 @@ "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.5", + "@mui/x-data-grid": "^8.18.0", "@mui/x-date-pickers": "^8.17.0", + "date-and-time": "^4.1.0", "dayjs": "^1.11.19", "is-cidr": "^6.0.1", "qrcode.react": "^4.2.0", diff --git a/matrixgw_frontend/src/routes/APITokensRoute.tsx b/matrixgw_frontend/src/routes/APITokensRoute.tsx index f6c4470..423c367 100644 --- a/matrixgw_frontend/src/routes/APITokensRoute.tsx +++ b/matrixgw_frontend/src/routes/APITokensRoute.tsx @@ -1,6 +1,8 @@ import AddIcon from "@mui/icons-material/Add"; import RefreshIcon from "@mui/icons-material/Refresh"; import { Alert, AlertTitle, IconButton, Tooltip } from "@mui/material"; +import type { GridColDef } from "@mui/x-data-grid"; +import { DataGrid } from "@mui/x-data-grid"; import { QRCodeCanvas } from "qrcode.react"; import React from "react"; import { APIClient } from "../api/ApiClient"; @@ -9,6 +11,7 @@ import { CreateTokenDialog } from "../dialogs/CreateTokenDialog"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { CopyTextChip } from "../widgets/CopyTextChip"; import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; +import { TimeWidget } from "../widgets/TimeWidget"; export function APITokensRoute(): React.ReactElement { const count = React.useRef(0); @@ -75,7 +78,9 @@ export function APITokensRoute(): React.ReactElement { ready={list !== undefined} load={load} errMsg="Failed to load the list of tokens!" - build={() => <>{list?.length} tokens} + build={() => ( + + )} /> ); @@ -117,3 +122,92 @@ function CreatedToken(p: { token: TokenWithSecret }): React.ReactElement { ); } + +function TokensListGrid(p: { + list: Token[]; + onReload: () => void; +}): React.ReactElement { + const columns: GridColDef<(typeof p.list)[number]>[] = [ + { field: "id", headerName: "ID", flex: 1 }, + { + field: "name", + headerName: "Name", + flex: 3, + }, + { + field: "networks", + headerName: "Networks restriction", + flex: 3, + renderCell(params) { + return ( + params.row.networks?.join(", ") ?? ( + Unrestricted + ) + ); + }, + }, + { + field: "created", + headerName: "Creation", + flex: 3, + renderCell(params) { + return ; + }, + }, + { + field: "last_used", + headerName: "Last usage", + flex: 3, + renderCell(params) { + return ; + }, + }, + { + field: "max_inactivity", + headerName: "Max inactivity", + flex: 3, + renderCell(params) { + return ; + }, + }, + { + field: "expiration", + headerName: "Expiration", + flex: 3, + renderCell(params) { + return ; + }, + }, + { + field: "read_only", + headerName: "Read only", + flex: 2, + type: "boolean", + }, + ]; + + if (p.list.length === 0) + return ( +
    + You do not have created any token yet! +
    + ); + + return ( + c.id} + isCellEditable={() => false} + isRowSelectable={() => false} + /> + ); +} diff --git a/matrixgw_frontend/src/widgets/TimeWidget.tsx b/matrixgw_frontend/src/widgets/TimeWidget.tsx new file mode 100644 index 0000000..253a0e0 --- /dev/null +++ b/matrixgw_frontend/src/widgets/TimeWidget.tsx @@ -0,0 +1,86 @@ +import { Tooltip } from "@mui/material"; +import { format } from "date-and-time"; +import { time } from "../utils/DateUtils"; + +export function formatDateTime(time: number): string { + const t = new Date(); + t.setTime(1000 * time); + return format(t, "DD/MM/YYYY HH:mm:ss"); +} + +export function formatDate(time: number): string { + const t = new Date(); + t.setTime(1000 * time); + return format(t, "DD/MM/YYYY"); +} + +export function timeDiff(a: number, b: number): string { + let diff = b - a; + + if (diff === 0) return "now"; + if (diff === 1) return "1 second"; + + if (diff < 60) { + return `${diff} seconds`; + } + + diff = Math.floor(diff / 60); + + if (diff === 1) return "1 minute"; + if (diff < 60) { + return `${diff} minutes`; + } + + diff = Math.floor(diff / 60); + + if (diff === 1) return "1 hour"; + if (diff < 24) { + return `${diff} hours`; + } + + const diffDays = Math.floor(diff / 24); + + if (diffDays === 1) return "1 day"; + if (diffDays < 31) { + return `${diffDays} days`; + } + + diff = Math.floor(diffDays / 31); + + if (diff < 12) { + return `${diff} month`; + } + + const diffYears = Math.floor(diffDays / 365); + + if (diffYears === 1) return "1 year"; + return `${diffYears} years`; +} + +export function timeDiffFromNow(t: number): string { + return timeDiff(t, time()); +} + +export function TimeWidget(p: { + time?: number; + isDuration?: boolean; + showDate?: boolean; +}): React.ReactElement { + if (!p.time) return <>; + return ( + + + {p.showDate + ? formatDate(p.time) + : p.isDuration + ? timeDiff(0, p.time) + : timeDiffFromNow(p.time)} + + + ); +} From b5832df746f54b079858986ac64de1edd42ef5d3 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 18 Nov 2025 14:51:05 +0100 Subject: [PATCH 035/124] Can delete API token from UI --- .../src/controllers/tokens_controller.rs | 16 +++- matrixgw_backend/src/main.rs | 4 + matrixgw_frontend/src/api/TokensApi.ts | 12 +++ .../src/routes/APITokensRoute.tsx | 88 ++++++++++++++++++- 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/matrixgw_backend/src/controllers/tokens_controller.rs b/matrixgw_backend/src/controllers/tokens_controller.rs index 49350e3..997fbca 100644 --- a/matrixgw_backend/src/controllers/tokens_controller.rs +++ b/matrixgw_backend/src/controllers/tokens_controller.rs @@ -1,7 +1,7 @@ use crate::controllers::HttpResult; use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; -use crate::users::{APIToken, BaseAPIToken}; -use actix_web::HttpResponse; +use crate::users::{APIToken, APITokenID, BaseAPIToken}; +use actix_web::{HttpResponse, web}; /// Create a new token pub async fn create(auth: AuthExtractor) -> HttpResult { @@ -34,3 +34,15 @@ pub async fn get_list(auth: AuthExtractor) -> HttpResult { .collect::>(), )) } + +#[derive(serde::Deserialize)] +pub struct TokenIDInPath { + id: APITokenID, +} + +/// Delete an API access token +pub async fn delete(auth: AuthExtractor, path: web::Path) -> HttpResult { + let token = APIToken::load(&auth.user.email, &path.id).await?; + token.delete(&auth.user.email).await?; + Ok(HttpResponse::Accepted().finish()) +} diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 7004740..4d5ec16 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -115,6 +115,10 @@ async fn main() -> std::io::Result<()> { // API Tokens controller .route("/api/token", web::post().to(tokens_controller::create)) .route("/api/tokens", web::get().to(tokens_controller::get_list)) + .route( + "/api/token/{id}", + web::delete().to(tokens_controller::delete), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_frontend/src/api/TokensApi.ts b/matrixgw_frontend/src/api/TokensApi.ts index 268a2ae..acddaf6 100644 --- a/matrixgw_frontend/src/api/TokensApi.ts +++ b/matrixgw_frontend/src/api/TokensApi.ts @@ -43,4 +43,16 @@ export class TokensApi { }) ).data; } + + /** + * Delete a token + */ + static async Delete(t: Token): Promise { + return ( + await APIClient.exec({ + uri: `/token/${t.id}`, + method: "DELETE", + }) + ).data; + } } diff --git a/matrixgw_frontend/src/routes/APITokensRoute.tsx b/matrixgw_frontend/src/routes/APITokensRoute.tsx index 423c367..af72f09 100644 --- a/matrixgw_frontend/src/routes/APITokensRoute.tsx +++ b/matrixgw_frontend/src/routes/APITokensRoute.tsx @@ -2,7 +2,7 @@ import AddIcon from "@mui/icons-material/Add"; import RefreshIcon from "@mui/icons-material/Refresh"; import { Alert, AlertTitle, IconButton, Tooltip } from "@mui/material"; import type { GridColDef } from "@mui/x-data-grid"; -import { DataGrid } from "@mui/x-data-grid"; +import { DataGrid, GridActionsCellItem } from "@mui/x-data-grid"; import { QRCodeCanvas } from "qrcode.react"; import React from "react"; import { APIClient } from "../api/ApiClient"; @@ -12,6 +12,11 @@ import { AsyncWidget } from "../widgets/AsyncWidget"; import { CopyTextChip } from "../widgets/CopyTextChip"; import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; import { TimeWidget } from "../widgets/TimeWidget"; +import DeleteIcon from "@mui/icons-material/DeleteOutlined"; +import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider"; +import { useConfirm } from "../hooks/contexts_provider/ConfirmDialogProvider"; +import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider"; +import { time } from "../utils/DateUtils"; export function APITokensRoute(): React.ReactElement { const count = React.useRef(0); @@ -127,6 +132,30 @@ function TokensListGrid(p: { list: Token[]; onReload: () => void; }): React.ReactElement { + const snackbar = useSnackbar(); + const confirm = useConfirm(); + const alert = useAlert(); + + // Delete a token + const handleDeleteClick = (token: Token) => async () => { + try { + if ( + !(await confirm( + `Do you really want to delete the token named '${token.name}' ?` + )) + ) + return; + + await TokensApi.Delete(token); + p.onReload(); + + snackbar("The token was successfully deleted!"); + } catch (e) { + console.error(e); + alert(`Failed to delete API token! ${e}`); + } + }; + const columns: GridColDef<(typeof p.list)[number]>[] = [ { field: "id", headerName: "ID", flex: 1 }, { @@ -159,7 +188,18 @@ function TokensListGrid(p: { headerName: "Last usage", flex: 3, renderCell(params) { - return ; + return ( + + + + ); }, }, { @@ -167,7 +207,18 @@ function TokensListGrid(p: { headerName: "Max inactivity", flex: 3, renderCell(params) { - return ; + return ( + + + + ); }, }, { @@ -175,7 +226,18 @@ function TokensListGrid(p: { headerName: "Expiration", flex: 3, renderCell(params) { - return ; + return ( + + + + ); }, }, { @@ -184,6 +246,24 @@ function TokensListGrid(p: { flex: 2, type: "boolean", }, + { + field: "actions", + type: "actions", + headerName: "Actions", + flex: 2, + cellClassName: "actions", + getActions: ({ row }) => { + return [ + } + label="Delete" + onClick={handleDeleteClick(row)} + color="inherit" + />, + ]; + }, + }, ]; if (p.list.length === 0) From 5c13cffe083a7a2a1c04a0fa25b812b51dcb210a Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 18 Nov 2025 15:09:27 +0100 Subject: [PATCH 036/124] Send broadcast message when an API token is deleted --- matrixgw_backend/src/broadcast_messages.rs | 8 +++++--- matrixgw_backend/src/controllers/auth_controller.rs | 9 +++++++-- matrixgw_backend/src/controllers/tokens_controller.rs | 9 +++++++-- matrixgw_backend/src/matrix_connection/matrix_manager.rs | 2 +- matrixgw_backend/src/users.rs | 8 +++++++- 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs index 423114f..e31174e 100644 --- a/matrixgw_backend/src/broadcast_messages.rs +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -1,10 +1,12 @@ -use crate::users::UserEmail; +use crate::users::{APIToken, UserEmail}; pub type BroadcastSender = tokio::sync::broadcast::Sender; /// Broadcast messages #[derive(Debug, Clone)] pub enum BroadcastMessage { - /// User is or has been disconnected - UserDisconnected(UserEmail), + /// User is or has been disconnected from Matrix + UserDisconnectedFromMatrix(UserEmail), + /// API token has been deleted + APITokenDeleted(APIToken), } diff --git a/matrixgw_backend/src/controllers/auth_controller.rs b/matrixgw_backend/src/controllers/auth_controller.rs index 42d759f..d9332b0 100644 --- a/matrixgw_backend/src/controllers/auth_controller.rs +++ b/matrixgw_backend/src/controllers/auth_controller.rs @@ -1,4 +1,5 @@ use crate::app_config::AppConfig; +use crate::broadcast_messages::BroadcastSender; use crate::controllers::{HttpFailure, HttpResult}; use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; @@ -113,14 +114,18 @@ pub async fn auth_info(client: MatrixClientExtractor) -> HttpResult { } /// Sign out user -pub async fn sign_out(auth: AuthExtractor, session: MatrixGWSession) -> HttpResult { +pub async fn sign_out( + auth: AuthExtractor, + session: MatrixGWSession, + tx: web::Data, +) -> HttpResult { match auth.method { AuthenticatedMethod::Cookie => { session.unset_current_user()?; } AuthenticatedMethod::Token(token) => { - token.delete(&auth.user.email).await?; + token.delete(&auth.user.email, &tx).await?; } AuthenticatedMethod::Dev => { diff --git a/matrixgw_backend/src/controllers/tokens_controller.rs b/matrixgw_backend/src/controllers/tokens_controller.rs index 997fbca..ce50d82 100644 --- a/matrixgw_backend/src/controllers/tokens_controller.rs +++ b/matrixgw_backend/src/controllers/tokens_controller.rs @@ -1,3 +1,4 @@ +use crate::broadcast_messages::BroadcastSender; use crate::controllers::HttpResult; use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; use crate::users::{APIToken, APITokenID, BaseAPIToken}; @@ -41,8 +42,12 @@ pub struct TokenIDInPath { } /// Delete an API access token -pub async fn delete(auth: AuthExtractor, path: web::Path) -> HttpResult { +pub async fn delete( + auth: AuthExtractor, + path: web::Path, + tx: web::Data, +) -> HttpResult { let token = APIToken::load(&auth.user.email, &path.id).await?; - token.delete(&auth.user.email).await?; + token.delete(&auth.user.email, &tx).await?; Ok(HttpResponse::Accepted().finish()) } diff --git a/matrixgw_backend/src/matrix_connection/matrix_manager.rs b/matrixgw_backend/src/matrix_connection/matrix_manager.rs index d75cab6..759d694 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_manager.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -67,7 +67,7 @@ impl Actor for MatrixManagerActor { } if let Err(e) = state .broadcast_sender - .send(BroadcastMessage::UserDisconnected(email)) + .send(BroadcastMessage::UserDisconnectedFromMatrix(email)) { log::warn!( "Failed to notify that user has been disconnected from Matrix! {e}" diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs index ad1e410..26f1f17 100644 --- a/matrixgw_backend/src/users.rs +++ b/matrixgw_backend/src/users.rs @@ -1,4 +1,5 @@ use crate::app_config::AppConfig; +use crate::broadcast_messages::{BroadcastMessage, BroadcastSender}; use crate::constants; use crate::controllers::server_controller::ServerConstraints; use crate::matrix_connection::matrix_client::EncryptionRecoveryState; @@ -246,9 +247,14 @@ impl APIToken { } /// Delete this token - pub async fn delete(self, email: &UserEmail) -> anyhow::Result<()> { + pub async fn delete(self, email: &UserEmail, tx: &BroadcastSender) -> anyhow::Result<()> { let token_file = AppConfig::get().user_api_token_metadata_file(email, &self.id); std::fs::remove_file(&token_file).map_err(MatrixGWUserError::DeleteToken)?; + + if let Err(e) = tx.send(BroadcastMessage::APITokenDeleted(self)) { + log::error!("Failed to notify API token deletion! {e}"); + } + Ok(()) } From c9b703bea3198149920da2da322a3843e4bdbf6c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 18 Nov 2025 22:17:39 +0100 Subject: [PATCH 037/124] Ready to implement sync thread logic --- matrixgw_backend/src/broadcast_messages.rs | 3 ++ .../matrix_sync_thread_controller.rs | 22 +++++++++++++ matrixgw_backend/src/controllers/mod.rs | 1 + matrixgw_backend/src/main.rs | 8 ++++- .../src/matrix_connection/matrix_manager.rs | 33 +++++++++++++++++++ matrixgw_backend/src/matrix_connection/mod.rs | 1 + .../src/matrix_connection/sync_thread.rs | 32 ++++++++++++++++++ matrixgw_frontend/src/api/MatrixSyncApi.ts | 13 ++++++++ matrixgw_frontend/src/routes/HomeRoute.tsx | 15 ++++++++- 9 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs create mode 100644 matrixgw_backend/src/matrix_connection/sync_thread.rs create mode 100644 matrixgw_frontend/src/api/MatrixSyncApi.ts diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs index e31174e..d83d2d3 100644 --- a/matrixgw_backend/src/broadcast_messages.rs +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -1,3 +1,4 @@ +use crate::matrix_connection::sync_thread::MatrixSyncTaskID; use crate::users::{APIToken, UserEmail}; pub type BroadcastSender = tokio::sync::broadcast::Sender; @@ -9,4 +10,6 @@ pub enum BroadcastMessage { UserDisconnectedFromMatrix(UserEmail), /// API token has been deleted APITokenDeleted(APIToken), + /// Request a Matrix sync thread to be interrupted + StopSyncThread(MatrixSyncTaskID), } diff --git a/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs b/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs new file mode 100644 index 0000000..bbfa799 --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs @@ -0,0 +1,22 @@ +use crate::controllers::HttpResult; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use crate::matrix_connection::matrix_manager::MatrixManagerMsg; +use actix_web::{HttpResponse, web}; +use ractor::ActorRef; + +/// Start sync thread +pub async fn start_sync( + client: MatrixClientExtractor, + manager: web::Data>, +) -> HttpResult { + match ractor::cast!( + manager, + MatrixManagerMsg::StartSyncThread(client.auth.user.email.clone()) + ) { + Ok(_) => Ok(HttpResponse::Accepted().finish()), + Err(e) => { + log::error!("Failed to start sync: {e}"); + Ok(HttpResponse::InternalServerError().finish()) + } + } +} diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 0fbc422..01d8545 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -4,6 +4,7 @@ use std::error::Error; pub mod auth_controller; pub mod matrix_link_controller; +pub mod matrix_sync_thread_controller; pub mod server_controller; pub mod tokens_controller; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 4d5ec16..9a98eb9 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -10,7 +10,8 @@ use matrixgw_backend::app_config::AppConfig; use matrixgw_backend::broadcast_messages::BroadcastMessage; use matrixgw_backend::constants; use matrixgw_backend::controllers::{ - auth_controller, matrix_link_controller, server_controller, tokens_controller, + auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller, + tokens_controller, }; use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor; use matrixgw_backend::users::User; @@ -119,6 +120,11 @@ async fn main() -> std::io::Result<()> { "/api/token/{id}", web::delete().to(tokens_controller::delete), ) + // Matrix synchronization controller + .route( + "/api/matrix_sync/start", + web::post().to(matrix_sync_thread_controller::start_sync), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/matrix_connection/matrix_manager.rs b/matrixgw_backend/src/matrix_connection/matrix_manager.rs index 759d694..fc80451 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_manager.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -1,5 +1,6 @@ use crate::broadcast_messages::{BroadcastMessage, BroadcastSender}; use crate::matrix_connection::matrix_client::MatrixClient; +use crate::matrix_connection::sync_thread::{MatrixSyncTaskID, start_sync_thread}; use crate::users::UserEmail; use ractor::{Actor, ActorProcessingErr, ActorRef, RpcReplyPort}; use std::collections::HashMap; @@ -7,11 +8,13 @@ use std::collections::HashMap; pub struct MatrixManagerState { pub broadcast_sender: BroadcastSender, pub clients: HashMap, + pub running_sync_threads: HashMap, } pub enum MatrixManagerMsg { GetClient(UserEmail, RpcReplyPort>), DisconnectClient(UserEmail), + StartSyncThread(UserEmail), } pub struct MatrixManagerActor; @@ -29,6 +32,7 @@ impl Actor for MatrixManagerActor { Ok(MatrixManagerState { broadcast_sender: args, clients: HashMap::new(), + running_sync_threads: Default::default(), }) } @@ -62,6 +66,15 @@ impl Actor for MatrixManagerActor { } MatrixManagerMsg::DisconnectClient(email) => { if let Some(c) = state.clients.remove(&email) { + // Stop sync thread (if running) + if let Some(id) = state.running_sync_threads.remove(&email) { + state + .broadcast_sender + .send(BroadcastMessage::StopSyncThread(id)) + .ok(); + } + + // Disconnect client if let Err(e) = c.disconnect().await { log::error!("Failed to disconnect client: {e}"); } @@ -75,6 +88,26 @@ impl Actor for MatrixManagerActor { } } } + MatrixManagerMsg::StartSyncThread(email) => { + // Do nothing if task is already running + if state.running_sync_threads.contains_key(&email) { + log::debug!("Not starting sync thread for {email:?} as it is already running"); + return Ok(()); + } + + let Some(client) = state.clients.get(&email) else { + log::warn!( + "Cannot start sync thread for {email:?} because client is not initialized!" + ); + return Ok(()); + }; + + // Start thread + log::debug!("Starting sync thread for {email:?}"); + let thread_id = + start_sync_thread(client.clone(), state.broadcast_sender.clone()).await?; + state.running_sync_threads.insert(email, thread_id); + } } Ok(()) } diff --git a/matrixgw_backend/src/matrix_connection/mod.rs b/matrixgw_backend/src/matrix_connection/mod.rs index c929f1b..29194b1 100644 --- a/matrixgw_backend/src/matrix_connection/mod.rs +++ b/matrixgw_backend/src/matrix_connection/mod.rs @@ -1,2 +1,3 @@ pub mod matrix_client; pub mod matrix_manager; +pub mod sync_thread; diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs new file mode 100644 index 0000000..9169cb6 --- /dev/null +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -0,0 +1,32 @@ +//! # Matrix sync thread +//! +//! This file contains the logic performed by the threads that synchronize with Matrix account. + +use crate::broadcast_messages::BroadcastSender; +use crate::matrix_connection::matrix_client::MatrixClient; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MatrixSyncTaskID(uuid::Uuid); + +/// Start synchronization thread for a given user +pub async fn start_sync_thread( + client: MatrixClient, + tx: BroadcastSender, +) -> anyhow::Result { + let task_id = MatrixSyncTaskID(uuid::Uuid::new_v4()); + let task_id_clone = task_id.clone(); + + tokio::task::spawn(async move { + sync_thread_task(task_id_clone, client, tx).await; + }); + + Ok(task_id) +} + +/// Sync thread function for a single function +async fn sync_thread_task(task_id: MatrixSyncTaskID, client: MatrixClient, _tx: BroadcastSender) { + loop { + println!("TODO : sync actions {task_id:?} {:?}", client.email); + tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + } +} diff --git a/matrixgw_frontend/src/api/MatrixSyncApi.ts b/matrixgw_frontend/src/api/MatrixSyncApi.ts new file mode 100644 index 0000000..600a4e6 --- /dev/null +++ b/matrixgw_frontend/src/api/MatrixSyncApi.ts @@ -0,0 +1,13 @@ +import { APIClient } from "./ApiClient"; + +export class MatrixSyncApi { + /** + * Force sync thread startup + */ + static async Start(): Promise { + await APIClient.exec({ + method: "POST", + uri: "/matrix_sync/start", + }); + } +} diff --git a/matrixgw_frontend/src/routes/HomeRoute.tsx b/matrixgw_frontend/src/routes/HomeRoute.tsx index ee0ddf1..f21be44 100644 --- a/matrixgw_frontend/src/routes/HomeRoute.tsx +++ b/matrixgw_frontend/src/routes/HomeRoute.tsx @@ -1,3 +1,6 @@ +import { APIClient } from "../api/ApiClient"; +import { MatrixSyncApi } from "../api/MatrixSyncApi"; +import { AsyncWidget } from "../widgets/AsyncWidget"; import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; import { NotLinkedAccountMessage } from "../widgets/NotLinkedAccountMessage"; @@ -6,5 +9,15 @@ export function HomeRoute(): React.ReactElement { if (!user.info.matrix_user_id) return ; - return

    Todo home route

    ; + return ( +

    + Todo home route{" "} + <>sync started} + /> +

    + ); } From 79d4482ea4a202432dce56bf7c7a2c481d826963 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Nov 2025 10:27:46 +0100 Subject: [PATCH 038/124] Sync threads can be interrupted --- .../src/matrix_connection/sync_thread.rs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs index 9169cb6..99af254 100644 --- a/matrixgw_backend/src/matrix_connection/sync_thread.rs +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -2,7 +2,7 @@ //! //! This file contains the logic performed by the threads that synchronize with Matrix account. -use crate::broadcast_messages::BroadcastSender; +use crate::broadcast_messages::{BroadcastMessage, BroadcastSender}; use crate::matrix_connection::matrix_client::MatrixClient; #[derive(Clone, Debug, Eq, PartialEq)] @@ -24,9 +24,29 @@ pub async fn start_sync_thread( } /// Sync thread function for a single function -async fn sync_thread_task(task_id: MatrixSyncTaskID, client: MatrixClient, _tx: BroadcastSender) { +async fn sync_thread_task(id: MatrixSyncTaskID, client: MatrixClient, tx: BroadcastSender) { + let mut rx = tx.subscribe(); + + log::info!("Sync thread {id:?} started for user {:?}", client.email); + loop { - println!("TODO : sync actions {task_id:?} {:?}", client.email); - tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + tokio::select! { + // Message from tokio broadcast + msg = rx.recv() => { + match msg { + Ok(BroadcastMessage::StopSyncThread(task_id)) if task_id == id => { + log::info!("A request was received to stop sync task! {id:?} for user {:?}", client.email); + break; + } + Err(e) => { + log::error!("Failed to receive a message from broadcast! {e}"); + return; + } + Ok(_) => {} + } + } + } } + + log::info!("Sync thread {id:?} terminated!"); } From 5bf7c7f8dfd933ffd25c3eae43184493f208b092 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Nov 2025 10:49:26 +0100 Subject: [PATCH 039/124] Do not start sync thread if user is disconnected --- matrixgw_backend/src/matrix_connection/matrix_client.rs | 5 +++++ matrixgw_backend/src/matrix_connection/matrix_manager.rs | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index b147ad7..f7f02f7 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -298,6 +298,11 @@ impl MatrixClient { Ok(()) } + /// Check whether a user is currently connected to this client or not + pub fn is_client_connected(&self) -> bool { + self.client.is_active() + } + /// Disconnect user from client pub async fn disconnect(self) -> anyhow::Result<()> { if let Err(e) = self.client.logout().await { diff --git a/matrixgw_backend/src/matrix_connection/matrix_manager.rs b/matrixgw_backend/src/matrix_connection/matrix_manager.rs index fc80451..4c6d940 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_manager.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -102,6 +102,13 @@ impl Actor for MatrixManagerActor { return Ok(()); }; + if !client.is_client_connected() { + log::warn!( + "Cannot start sync thread for {email:?} because Matrix account is not set!" + ); + return Ok(()); + } + // Start thread log::debug!("Starting sync thread for {email:?}"); let thread_id = From 07f6544a4a42e99d75a77e1b64c90cb2f6247d77 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Nov 2025 11:37:57 +0100 Subject: [PATCH 040/124] WIP sync thread implementation --- matrixgw_backend/src/broadcast_messages.rs | 2 ++ .../src/extractors/matrix_client_extractor.rs | 4 +-- .../src/matrix_connection/matrix_client.rs | 36 ++++++++++++++++++- .../src/matrix_connection/sync_thread.rs | 23 +++++++++++- 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs index d83d2d3..18328e1 100644 --- a/matrixgw_backend/src/broadcast_messages.rs +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -12,4 +12,6 @@ pub enum BroadcastMessage { APITokenDeleted(APIToken), /// Request a Matrix sync thread to be interrupted StopSyncThread(MatrixSyncTaskID), + /// Matrix sync thread has been interrupted + SyncThreadStopped(MatrixSyncTaskID), } diff --git a/matrixgw_backend/src/extractors/matrix_client_extractor.rs b/matrixgw_backend/src/extractors/matrix_client_extractor.rs index 545db6c..5a25e06 100644 --- a/matrixgw_backend/src/extractors/matrix_client_extractor.rs +++ b/matrixgw_backend/src/extractors/matrix_client_extractor.rs @@ -15,8 +15,8 @@ impl MatrixClientExtractor { pub async fn to_extended_user_info(&self) -> anyhow::Result { Ok(ExtendedUserInfo { user: self.auth.user.clone(), - matrix_user_id: self.client.client.user_id().map(|id| id.to_string()), - matrix_device_id: self.client.client.device_id().map(|id| id.to_string()), + matrix_user_id: self.client.user_id().map(|id| id.to_string()), + matrix_device_id: self.client.device_id().map(|id| id.to_string()), matrix_recovery_state: self.client.recovery_state(), }) } diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index f7f02f7..58677a8 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -3,15 +3,21 @@ use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use crate::users::UserEmail; use crate::utils::rand_utils::rand_string; use anyhow::Context; +use futures_util::Stream; use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError; use matrix_sdk::authentication::oauth::{ ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession, }; +use matrix_sdk::config::SyncSettings; use matrix_sdk::encryption::recovery::RecoveryState; +use matrix_sdk::ruma::presence::PresenceState; use matrix_sdk::ruma::serde::Raw; +use matrix_sdk::ruma::{DeviceId, UserId}; +use matrix_sdk::sync::SyncResponse; use matrix_sdk::{Client, ClientBuildError}; use ractor::ActorRef; use serde::{Deserialize, Serialize}; +use std::pin::Pin; use url::Url; /// The full session to persist. @@ -83,7 +89,7 @@ pub struct FinishMatrixAuth { pub struct MatrixClient { manager: ActorRef, pub email: UserEmail, - pub client: Client, + client: Client, } impl MatrixClient { @@ -315,6 +321,16 @@ impl MatrixClient { Ok(()) } + /// Get client Matrix device id + pub fn device_id(&self) -> Option<&DeviceId> { + self.client.device_id() + } + + /// Get client Matrix user id + pub fn user_id(&self) -> Option<&UserId> { + self.client.user_id() + } + /// Get current encryption keys recovery state pub fn recovery_state(&self) -> EncryptionRecoveryState { match self.client.encryption().recovery().state() { @@ -335,4 +351,22 @@ impl MatrixClient { .await .map_err(MatrixClientError::SetRecoveryKey)?) } + + /// Get matrix synchronization settings to use + fn sync_settings() -> SyncSettings { + SyncSettings::default().set_presence(PresenceState::Offline) + } + + /// Perform initial synchronization + pub async fn perform_initial_sync(&self) -> anyhow::Result<()> { + self.client.sync_once(Self::sync_settings()).await?; + Ok(()) + } + + /// Perform routine synchronization + pub async fn sync_stream( + &self, + ) -> Pin>>> { + Box::pin(self.client.sync_stream(Self::sync_settings()).await) + } } diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs index 99af254..ac9b4e2 100644 --- a/matrixgw_backend/src/matrix_connection/sync_thread.rs +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -4,6 +4,7 @@ use crate::broadcast_messages::{BroadcastMessage, BroadcastSender}; use crate::matrix_connection::matrix_client::MatrixClient; +use futures_util::StreamExt; #[derive(Clone, Debug, Eq, PartialEq)] pub struct MatrixSyncTaskID(uuid::Uuid); @@ -29,6 +30,14 @@ async fn sync_thread_task(id: MatrixSyncTaskID, client: MatrixClient, tx: Broadc log::info!("Sync thread {id:?} started for user {:?}", client.email); + log::info!("Perform initial synchronization..."); + if let Err(e) = client.perform_initial_sync().await { + log::error!("Failed to perform initial Matrix synchronization! {e:?}"); + return; + } + + let mut sync_stream = client.sync_stream().await; + loop { tokio::select! { // Message from tokio broadcast @@ -40,13 +49,25 @@ async fn sync_thread_task(id: MatrixSyncTaskID, client: MatrixClient, tx: Broadc } Err(e) => { log::error!("Failed to receive a message from broadcast! {e}"); - return; + break; } Ok(_) => {} } } + + evt = sync_stream.next() => { + let Some(evt)= evt else { + log::error!("No more Matrix event to process, stopping now..."); + break; + }; + + println!("Sync thread {id:?} event: {:?}", evt); + } } } log::info!("Sync thread {id:?} terminated!"); + if let Err(e) = tx.send(BroadcastMessage::SyncThreadStopped(id)) { + log::warn!("Failed to notify that synchronization thread has been interrupted! {e}") + } } From 75b6b224bcb6a01f5438de6f08e6b2922670ce4a Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Nov 2025 13:39:28 +0100 Subject: [PATCH 041/124] Notify Matrix manager directly if sync thread is terminated --- .../src/matrix_connection/matrix_client.rs | 58 ++++++++++++++----- .../src/matrix_connection/matrix_manager.rs | 12 +++- .../src/matrix_connection/sync_thread.rs | 23 +++++++- 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index 58677a8..dc59884 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -10,12 +10,14 @@ use matrix_sdk::authentication::oauth::{ }; use matrix_sdk::config::SyncSettings; use matrix_sdk::encryption::recovery::RecoveryState; +use matrix_sdk::event_handler::{EventHandler, EventHandlerHandle, SyncEvent}; use matrix_sdk::ruma::presence::PresenceState; use matrix_sdk::ruma::serde::Raw; use matrix_sdk::ruma::{DeviceId, UserId}; use matrix_sdk::sync::SyncResponse; -use matrix_sdk::{Client, ClientBuildError}; +use matrix_sdk::{Client, ClientBuildError, SendOutsideWasm}; use ractor::ActorRef; +use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::pin::Pin; use url::Url; @@ -255,23 +257,32 @@ impl MatrixClient { pub async fn setup_background_session_save(&self) { let this = self.clone(); tokio::spawn(async move { - while let Ok(update) = this.client.subscribe_to_session_changes().recv().await { - match update { - matrix_sdk::SessionChange::UnknownToken { soft_logout } => { - log::warn!("Received an unknown token error; soft logout? {soft_logout:?}"); - if let Err(e) = this - .manager - .cast(MatrixManagerMsg::DisconnectClient(this.email)) - { - log::warn!("Failed to propagate invalid token error: {e}"); + loop { + match this.client.subscribe_to_session_changes().recv().await { + Ok(update) => match update { + matrix_sdk::SessionChange::UnknownToken { soft_logout } => { + log::warn!( + "Received an unknown token error; soft logout? {soft_logout:?}" + ); + if let Err(e) = this + .manager + .cast(MatrixManagerMsg::DisconnectClient(this.email)) + { + log::warn!("Failed to propagate invalid token error: {e}"); + } + break; } - break; - } - matrix_sdk::SessionChange::TokensRefreshed => { - // The tokens have been refreshed, persist them to disk. - if let Err(err) = this.save_stored_session().await { - log::error!("Unable to store a session in the background: {err}"); + matrix_sdk::SessionChange::TokensRefreshed => { + // The tokens have been refreshed, persist them to disk. + if let Err(err) = this.save_stored_session().await { + log::error!("Unable to store a session in the background: {err}"); + } } + }, + Err(e) => { + log::error!("[!] Session change error: {e}"); + log::error!("Session change background service INTERRUPTED!"); + return; } } } @@ -369,4 +380,19 @@ impl MatrixClient { ) -> Pin>>> { Box::pin(self.client.sync_stream(Self::sync_settings()).await) } + + /// Add new Matrix event handler + #[must_use] + pub fn add_event_handler(&self, handler: H) -> EventHandlerHandle + where + Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static, + H: EventHandler, + { + self.client.add_event_handler(handler) + } + + /// Remove Matrix event handler + pub fn remove_event_handler(&self, handle: EventHandlerHandle) { + self.client.remove_event_handler(handle) + } } diff --git a/matrixgw_backend/src/matrix_connection/matrix_manager.rs b/matrixgw_backend/src/matrix_connection/matrix_manager.rs index 4c6d940..c7a3ccb 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_manager.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -15,6 +15,7 @@ pub enum MatrixManagerMsg { GetClient(UserEmail, RpcReplyPort>), DisconnectClient(UserEmail), StartSyncThread(UserEmail), + SyncThreadTerminated(UserEmail, MatrixSyncTaskID), } pub struct MatrixManagerActor; @@ -112,9 +113,18 @@ impl Actor for MatrixManagerActor { // Start thread log::debug!("Starting sync thread for {email:?}"); let thread_id = - start_sync_thread(client.clone(), state.broadcast_sender.clone()).await?; + start_sync_thread(client.clone(), state.broadcast_sender.clone(), myself) + .await?; state.running_sync_threads.insert(email, thread_id); } + MatrixManagerMsg::SyncThreadTerminated(email, task_id) => { + if state.running_sync_threads.get(&email) == Some(&task_id) { + log::info!( + "Sync thread {task_id:?} has been terminated, removing it from the list..." + ); + state.running_sync_threads.remove(&email); + } + } } Ok(()) } diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs index ac9b4e2..e8f07a8 100644 --- a/matrixgw_backend/src/matrix_connection/sync_thread.rs +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -4,7 +4,9 @@ use crate::broadcast_messages::{BroadcastMessage, BroadcastSender}; use crate::matrix_connection::matrix_client::MatrixClient; +use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use futures_util::StreamExt; +use ractor::ActorRef; #[derive(Clone, Debug, Eq, PartialEq)] pub struct MatrixSyncTaskID(uuid::Uuid); @@ -13,19 +15,25 @@ pub struct MatrixSyncTaskID(uuid::Uuid); pub async fn start_sync_thread( client: MatrixClient, tx: BroadcastSender, + manager: ActorRef, ) -> anyhow::Result { let task_id = MatrixSyncTaskID(uuid::Uuid::new_v4()); let task_id_clone = task_id.clone(); tokio::task::spawn(async move { - sync_thread_task(task_id_clone, client, tx).await; + sync_thread_task(task_id_clone, client, tx, manager).await; }); Ok(task_id) } /// Sync thread function for a single function -async fn sync_thread_task(id: MatrixSyncTaskID, client: MatrixClient, tx: BroadcastSender) { +async fn sync_thread_task( + id: MatrixSyncTaskID, + client: MatrixClient, + tx: BroadcastSender, + manager: ActorRef, +) { let mut rx = tx.subscribe(); log::info!("Sync thread {id:?} started for user {:?}", client.email); @@ -38,6 +46,8 @@ async fn sync_thread_task(id: MatrixSyncTaskID, client: MatrixClient, tx: Broadc let mut sync_stream = client.sync_stream().await; + //let room_message_handle = client.add_event_handler(); + loop { tokio::select! { // Message from tokio broadcast @@ -66,7 +76,16 @@ async fn sync_thread_task(id: MatrixSyncTaskID, client: MatrixClient, tx: Broadc } } + //client.remove_event_handler(room_message_handle); + + // Notify manager about termination, so this thread can be removed from the list log::info!("Sync thread {id:?} terminated!"); + if let Err(e) = ractor::cast!( + manager, + MatrixManagerMsg::SyncThreadTerminated(client.email.clone(), id.clone()) + ) { + log::error!("Failed to notify Matrix manager about thread termination! {e}"); + } if let Err(e) = tx.send(BroadcastMessage::SyncThreadStopped(id)) { log::warn!("Failed to notify that synchronization thread has been interrupted! {e}") } From cfdf98b47a6341bee863e1e5a72252e07ff63618 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Nov 2025 14:02:51 +0100 Subject: [PATCH 042/124] Matrix messages are broadcasted --- matrixgw_backend/src/broadcast_messages.rs | 11 ++++ .../src/matrix_connection/sync_thread.rs | 51 ++++++++++++++----- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs index 18328e1..b5f9407 100644 --- a/matrixgw_backend/src/broadcast_messages.rs +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -1,5 +1,8 @@ use crate::matrix_connection::sync_thread::MatrixSyncTaskID; use crate::users::{APIToken, UserEmail}; +use matrix_sdk::Room; +use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent; +use matrix_sdk::sync::SyncResponse; pub type BroadcastSender = tokio::sync::broadcast::Sender; @@ -14,4 +17,12 @@ pub enum BroadcastMessage { StopSyncThread(MatrixSyncTaskID), /// Matrix sync thread has been interrupted SyncThreadStopped(MatrixSyncTaskID), + /// New room message + RoomMessageEvent { + user: UserEmail, + event: Box, + room: Room, + }, + /// Raw Matrix sync response + MatrixSyncResponse { user: UserEmail, sync: SyncResponse }, } diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs index e8f07a8..223fbc3 100644 --- a/matrixgw_backend/src/matrix_connection/sync_thread.rs +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -6,6 +6,8 @@ use crate::broadcast_messages::{BroadcastMessage, BroadcastSender}; use crate::matrix_connection::matrix_client::MatrixClient; use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use futures_util::StreamExt; +use matrix_sdk::Room; +use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent; use ractor::ActorRef; #[derive(Clone, Debug, Eq, PartialEq)] @@ -17,6 +19,13 @@ pub async fn start_sync_thread( tx: BroadcastSender, manager: ActorRef, ) -> anyhow::Result { + // Perform initial synchronization here, so in case of error the sync task does not get registered + log::info!("Perform initial synchronization..."); + if let Err(e) = client.perform_initial_sync().await { + log::error!("Failed to perform initial Matrix synchronization! {e:?}"); + return Err(e); + } + let task_id = MatrixSyncTaskID(uuid::Uuid::new_v4()); let task_id_clone = task_id.clone(); @@ -35,18 +44,23 @@ async fn sync_thread_task( manager: ActorRef, ) { let mut rx = tx.subscribe(); - log::info!("Sync thread {id:?} started for user {:?}", client.email); - log::info!("Perform initial synchronization..."); - if let Err(e) = client.perform_initial_sync().await { - log::error!("Failed to perform initial Matrix synchronization! {e:?}"); - return; - } - let mut sync_stream = client.sync_stream().await; - //let room_message_handle = client.add_event_handler(); + let tx_msg_handle = tx.clone(); + let user = client.email.clone(); + let room_message_handle = client.add_event_handler( + async move |event: OriginalSyncRoomMessageEvent, room: Room| { + if let Err(e) = tx_msg_handle.send(BroadcastMessage::RoomMessageEvent { + user: user.clone(), + event: Box::new(event), + room, + }) { + log::warn!("Failed to forward room event! {e}"); + } + }, + ); loop { tokio::select! { @@ -65,18 +79,31 @@ async fn sync_thread_task( } } - evt = sync_stream.next() => { - let Some(evt)= evt else { + res = sync_stream.next() => { + let Some(res)= res else { log::error!("No more Matrix event to process, stopping now..."); break; }; - println!("Sync thread {id:?} event: {:?}", evt); + // Forward message + match res { + Ok(res) => { + if let Err(e)= tx.send(BroadcastMessage::MatrixSyncResponse { + user: client.email.clone(), + sync: res + }) { + log::warn!("Failed to forward room event! {e}"); + } + } + Err(e) => { + log::error!("Sync error for user {:?}! {e}", client.email); + } + } } } } - //client.remove_event_handler(room_message_handle); + client.remove_event_handler(room_message_handle); // Notify manager about termination, so this thread can be removed from the list log::info!("Sync thread {id:?} terminated!"); From 1e00d24a8beab558373ebf41fc8f87172236a005 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Nov 2025 15:51:15 +0100 Subject: [PATCH 043/124] Can request sync thread stop --- .../matrix_sync_thread_controller.rs | 17 +++++++++++++++++ matrixgw_backend/src/main.rs | 4 ++++ .../src/matrix_connection/matrix_manager.rs | 10 ++++++++++ 3 files changed, 31 insertions(+) diff --git a/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs b/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs index bbfa799..ee76bfc 100644 --- a/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs +++ b/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs @@ -20,3 +20,20 @@ pub async fn start_sync( } } } + +/// Stop sync thread +pub async fn stop_sync( + client: MatrixClientExtractor, + manager: web::Data>, +) -> HttpResult { + match ractor::cast!( + manager, + MatrixManagerMsg::StopSyncThread(client.auth.user.email.clone()) + ) { + Ok(_) => Ok(HttpResponse::Accepted().finish()), + Err(e) => { + log::error!("Failed to stop sync thread: {e}"); + Ok(HttpResponse::InternalServerError().finish()) + } + } +} diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 9a98eb9..72e98d9 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -125,6 +125,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix_sync/start", web::post().to(matrix_sync_thread_controller::start_sync), ) + .route( + "/api/matrix_sync/stop", + web::post().to(matrix_sync_thread_controller::stop_sync), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/matrix_connection/matrix_manager.rs b/matrixgw_backend/src/matrix_connection/matrix_manager.rs index c7a3ccb..da25d39 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_manager.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -15,6 +15,7 @@ pub enum MatrixManagerMsg { GetClient(UserEmail, RpcReplyPort>), DisconnectClient(UserEmail), StartSyncThread(UserEmail), + StopSyncThread(UserEmail), SyncThreadTerminated(UserEmail, MatrixSyncTaskID), } @@ -117,6 +118,15 @@ impl Actor for MatrixManagerActor { .await?; state.running_sync_threads.insert(email, thread_id); } + MatrixManagerMsg::StopSyncThread(email) => { + if let Some(thread_id) = state.running_sync_threads.get(&email) + && let Err(e) = state + .broadcast_sender + .send(BroadcastMessage::StopSyncThread(thread_id.clone())) + { + log::error!("Failed to request sync thread stop: {e}"); + } + } MatrixManagerMsg::SyncThreadTerminated(email, task_id) => { if state.running_sync_threads.get(&email) == Some(&task_id) { log::info!( From 7b691962a0b23592e49247bce08a6ce25fe2da5d Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Nov 2025 16:34:00 +0100 Subject: [PATCH 044/124] Can get sync thread status --- .../matrix_sync_thread_controller.rs | 20 +++++++++++++++++++ .../src/extractors/matrix_client_extractor.rs | 10 +++++++--- matrixgw_backend/src/main.rs | 4 ++++ .../src/matrix_connection/matrix_manager.rs | 7 +++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs b/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs index ee76bfc..a071701 100644 --- a/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs +++ b/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs @@ -37,3 +37,23 @@ pub async fn stop_sync( } } } + +#[derive(serde::Serialize)] +struct GetSyncStatusResponse { + started: bool, +} + +/// Get sync thread status +pub async fn status( + client: MatrixClientExtractor, + manager: web::Data>, +) -> HttpResult { + let started = ractor::call!( + manager.as_ref(), + MatrixManagerMsg::SyncThreadGetStatus, + client.auth.user.email + ) + .expect("RPC to Matrix Manager failed"); + + Ok(HttpResponse::Ok().json(GetSyncStatusResponse { started })) +} diff --git a/matrixgw_backend/src/extractors/matrix_client_extractor.rs b/matrixgw_backend/src/extractors/matrix_client_extractor.rs index 5a25e06..0a80c3e 100644 --- a/matrixgw_backend/src/extractors/matrix_client_extractor.rs +++ b/matrixgw_backend/src/extractors/matrix_client_extractor.rs @@ -39,9 +39,13 @@ impl FromRequest for MatrixClientExtractor { matrix_manager_actor, MatrixManagerMsg::GetClient, auth.user.email.clone() - ) - .expect("Failed to query manager actor!") - .expect("Failed to get client!"); + ); + + let client = match client { + Ok(Ok(client)) => client, + Ok(Err(err)) => panic!("Failed to get client! {err:?}"), + Err(err) => panic!("Failed to query manager actor! {err:#?}"), + }; Ok(Self { auth, client }) }) diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 72e98d9..f873bf8 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -129,6 +129,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix_sync/stop", web::post().to(matrix_sync_thread_controller::stop_sync), ) + .route( + "/api/matrix_sync/status", + web::get().to(matrix_sync_thread_controller::status), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/matrix_connection/matrix_manager.rs b/matrixgw_backend/src/matrix_connection/matrix_manager.rs index da25d39..68415b9 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_manager.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -16,6 +16,7 @@ pub enum MatrixManagerMsg { DisconnectClient(UserEmail), StartSyncThread(UserEmail), StopSyncThread(UserEmail), + SyncThreadGetStatus(UserEmail, RpcReplyPort), SyncThreadTerminated(UserEmail, MatrixSyncTaskID), } @@ -127,6 +128,12 @@ impl Actor for MatrixManagerActor { log::error!("Failed to request sync thread stop: {e}"); } } + MatrixManagerMsg::SyncThreadGetStatus(email, reply) => { + let started = state.running_sync_threads.contains_key(&email); + if let Err(e) = reply.send(started) { + log::error!("Failed to send sync thread status! {e}"); + } + } MatrixManagerMsg::SyncThreadTerminated(email, task_id) => { if state.running_sync_threads.get(&email) == Some(&task_id) { log::info!( From 564e606ac7c6b34f3029e9b912797853666f3eef Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Nov 2025 17:15:54 +0100 Subject: [PATCH 045/124] Properly handle start sync thread issue --- .../src/matrix_connection/matrix_manager.rs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/matrix_manager.rs b/matrixgw_backend/src/matrix_connection/matrix_manager.rs index 68415b9..77fc707 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_manager.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -39,6 +39,15 @@ impl Actor for MatrixManagerActor { }) } + async fn post_stop( + &self, + _myself: ActorRef, + _state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + log::error!("[!] [!] Matrix Manager Actor stopped!"); + Ok(()) + } + async fn handle( &self, myself: ActorRef, @@ -115,8 +124,15 @@ impl Actor for MatrixManagerActor { // Start thread log::debug!("Starting sync thread for {email:?}"); let thread_id = - start_sync_thread(client.clone(), state.broadcast_sender.clone(), myself) - .await?; + match start_sync_thread(client.clone(), state.broadcast_sender.clone(), myself) + .await + { + Ok(thread_id) => thread_id, + Err(e) => { + log::error!("Failed to start sync thread! {e}"); + return Ok(()); + } + }; state.running_sync_threads.insert(email, thread_id); } MatrixManagerMsg::StopSyncThread(email) => { From 0d8905d842d9cf3483bc85848e4bdaae31d5e38a Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 19 Nov 2025 18:41:26 +0100 Subject: [PATCH 046/124] Can stop sync thread from UI --- .../src/matrix_connection/matrix_client.rs | 2 +- matrixgw_frontend/src/api/MatrixSyncApi.ts | 23 ++- matrixgw_frontend/src/routes/HomeRoute.tsx | 1 - .../src/routes/MatrixLinkRoute.tsx | 133 +++++++++++++++++- 4 files changed, 152 insertions(+), 7 deletions(-) diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index dc59884..eb7702e 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -121,7 +121,7 @@ impl MatrixClient { .map_err(MatrixClientError::ReadDbPassphrase)?; let client = Client::builder() - .server_name_or_homeserver_url(&AppConfig::get().matrix_homeserver) + .homeserver_url(&AppConfig::get().matrix_homeserver) // Automatically refresh tokens if needed .handle_refresh_tokens() .sqlite_store(&db_path, Some(&passphrase)) diff --git a/matrixgw_frontend/src/api/MatrixSyncApi.ts b/matrixgw_frontend/src/api/MatrixSyncApi.ts index 600a4e6..8e8999c 100644 --- a/matrixgw_frontend/src/api/MatrixSyncApi.ts +++ b/matrixgw_frontend/src/api/MatrixSyncApi.ts @@ -2,7 +2,7 @@ import { APIClient } from "./ApiClient"; export class MatrixSyncApi { /** - * Force sync thread startup + * Start sync thread */ static async Start(): Promise { await APIClient.exec({ @@ -10,4 +10,25 @@ export class MatrixSyncApi { uri: "/matrix_sync/start", }); } + + /** + * Stop sync thread + */ + static async Stop(): Promise { + await APIClient.exec({ + method: "POST", + uri: "/matrix_sync/stop", + }); + } + + /** + * Get sync thread status + */ + static async Status(): Promise { + const res = await APIClient.exec({ + method: "GET", + uri: "/matrix_sync/status", + }); + return res.data.started; + } } diff --git a/matrixgw_frontend/src/routes/HomeRoute.tsx b/matrixgw_frontend/src/routes/HomeRoute.tsx index f21be44..e404074 100644 --- a/matrixgw_frontend/src/routes/HomeRoute.tsx +++ b/matrixgw_frontend/src/routes/HomeRoute.tsx @@ -1,4 +1,3 @@ -import { APIClient } from "../api/ApiClient"; import { MatrixSyncApi } from "../api/MatrixSyncApi"; import { AsyncWidget } from "../widgets/AsyncWidget"; import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; diff --git a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx index 1bb675d..82173bf 100644 --- a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx +++ b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx @@ -3,15 +3,21 @@ import CloseIcon from "@mui/icons-material/Close"; import KeyIcon from "@mui/icons-material/Key"; import LinkIcon from "@mui/icons-material/Link"; import LinkOffIcon from "@mui/icons-material/LinkOff"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew"; +import StopIcon from "@mui/icons-material/Stop"; import { Button, Card, CardActions, CardContent, + CircularProgress, + Grid, Typography, } from "@mui/material"; import React from "react"; import { MatrixLinkApi } from "../api/MatrixLinkApi"; +import { MatrixSyncApi } from "../api/MatrixSyncApi"; import { SetRecoveryKeyDialog } from "../dialogs/SetRecoveryKeyDialog"; import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider"; import { useConfirm } from "../hooks/contexts_provider/ConfirmDialogProvider"; @@ -27,10 +33,17 @@ export function MatrixLinkRoute(): React.ReactElement { {user.info.matrix_user_id === null ? ( ) : ( - <> - - - + + + + + + + + + + + )} ); @@ -202,3 +215,115 @@ function EncryptionKeyStatus(): React.ReactElement { ); } + +function SyncThreadStatus(): React.ReactElement { + const alert = useAlert(); + const snackbar = useSnackbar(); + + const [started, setStarted] = React.useState(); + + const loadStatus = async () => { + try { + setStarted(await MatrixSyncApi.Status()); + } catch (e) { + console.error(`Failed to refresh sync thread status! ${e}`); + snackbar(`Failed to refresh sync thread status! ${e}`); + } + }; + + const handleStartThread = async () => { + try { + setStarted(undefined); + await MatrixSyncApi.Start(); + snackbar("Sync thread started"); + } catch (e) { + console.error(`Failed to start sync thread! ${e}`); + alert(`Failed to start sync thread! ${e}`); + } + }; + + const handleStopThread = async () => { + try { + setStarted(undefined); + await MatrixSyncApi.Stop(); + snackbar("Sync thread stopped"); + } catch (e) { + console.error(`Failed to stop sync thread! ${e}`); + alert(`Failed to stop sync thread! ${e}`); + } + }; + + React.useEffect(() => { + const interval = setInterval(loadStatus, 1000); + + () => clearInterval(interval); + }, []); + + return ( + <> + + + + Sync thread status + + +

    + A thread is spawned on the server to watch for events on the + Matrix server. You can restart this thread from here in case of + issue. +

    + +

    + Current thread status:{" "} + {started === undefined ? ( + <> + + + ) : started === true ? ( + <> + {" "} + Started + + ) : ( + <> + + Stopped + + )} +

    +
    +
    + + {started === false && ( + + )} + + {started === true && ( + + )} + +
    + + ); +} From a1b22699e916f0458ba184a3d655302443e0a1f1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Nov 2025 16:06:00 +0100 Subject: [PATCH 047/124] Basic implementation of websocket --- matrixgw_backend/Cargo.lock | 15 ++ matrixgw_backend/Cargo.toml | 3 +- matrixgw_backend/src/constants.rs | 10 + matrixgw_backend/src/controllers/mod.rs | 3 + .../src/controllers/ws_controller.rs | 198 ++++++++++++++++++ .../src/extractors/auth_extractor.rs | 10 + matrixgw_backend/src/main.rs | 3 +- 7 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 matrixgw_backend/src/controllers/ws_controller.rs diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index 33a12ff..46d938c 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -241,6 +241,20 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-ws" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99" +dependencies = [ + "actix-codec", + "actix-http", + "actix-web", + "bytestring", + "futures-core", + "tokio", +] + [[package]] name = "adler2" version = "2.0.1" @@ -3026,6 +3040,7 @@ dependencies = [ "actix-remote-ip", "actix-session", "actix-web", + "actix-ws", "anyhow", "base16ct 0.3.0", "bytes", diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index 9474195..f3b32d1 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -32,4 +32,5 @@ matrix-sdk = "0.14.0" url = "2.5.7" ractor = "0.15.9" serde_json = "1.0.145" -lazy-regex = "3.4.2" \ No newline at end of file +lazy-regex = "3.4.2" +actix-ws = "0.3.0" \ No newline at end of file diff --git a/matrixgw_backend/src/constants.rs b/matrixgw_backend/src/constants.rs index 2b8b86a..93ee5b7 100644 --- a/matrixgw_backend/src/constants.rs +++ b/matrixgw_backend/src/constants.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + /// Auth header pub const API_AUTH_HEADER: &str = "x-client-auth"; @@ -16,3 +18,11 @@ pub mod sessions { /// Authenticated ID pub const USER_ID: &str = "uid"; } + +/// How often heartbeat pings are sent. +/// +/// Should be half (or less) of the acceptable client timeout. +pub const WS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); + +/// How long before lack of client response causes a timeout. +pub const WS_CLIENT_TIMEOUT: Duration = Duration::from_secs(10); diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 01d8545..f457c29 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -7,6 +7,7 @@ pub mod matrix_link_controller; pub mod matrix_sync_thread_controller; pub mod server_controller; pub mod tokens_controller; +pub mod ws_controller; #[derive(thiserror::Error, Debug)] pub enum HttpFailure { @@ -18,6 +19,8 @@ pub enum HttpFailure { OpenID(Box), #[error("an unspecified internal error occurred: {0}")] InternalError(#[from] anyhow::Error), + #[error("Actix web error: {0}")] + ActixError(#[from] actix_web::Error), } impl ResponseError for HttpFailure { diff --git a/matrixgw_backend/src/controllers/ws_controller.rs b/matrixgw_backend/src/controllers/ws_controller.rs new file mode 100644 index 0000000..9ac0092 --- /dev/null +++ b/matrixgw_backend/src/controllers/ws_controller.rs @@ -0,0 +1,198 @@ +use crate::broadcast_messages::BroadcastMessage; +use crate::constants; +use crate::controllers::HttpResult; +use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use crate::matrix_connection::matrix_client::MatrixClient; +use crate::matrix_connection::matrix_manager::MatrixManagerMsg; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest, web}; +use actix_ws::Message; +use futures_util::StreamExt; +use matrix_sdk::ruma::OwnedRoomId; +use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; +use ractor::ActorRef; +use std::time::Instant; +use tokio::sync::broadcast; +use tokio::sync::broadcast::Receiver; +use tokio::time::interval; + +/// Messages sent to the client +#[derive(Debug, serde::Serialize)] +#[serde(tag = "type")] +pub enum WsMessage { + /// Room message event + RoomMessageEvent { + event: RoomMessageEventContent, + room_id: OwnedRoomId, + }, +} + +/// Main WS route +pub async fn ws( + req: HttpRequest, + stream: web::Payload, + tx: web::Data>, + manager: web::Data>, +) -> HttpResult { + // Forcefully ignore request payload by manually extracting authentication information + let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?; + + // Ensure sync thread is started + ractor::cast!( + manager, + MatrixManagerMsg::StartSyncThread(client.auth.user.email.clone()) + ) + .expect("Failed to start sync thread prior to running WebSocket!"); + + let rx = tx.subscribe(); + + let (res, session, msg_stream) = actix_ws::handle(&req, stream)?; + + // spawn websocket handler (and don't await it) so that the response is returned immediately + actix_web::rt::spawn(ws_handler( + session, + msg_stream, + client.auth, + client.client, + rx, + )); + + Ok(res) +} + +pub async fn ws_handler( + mut session: actix_ws::Session, + mut msg_stream: actix_ws::MessageStream, + auth: AuthExtractor, + client: MatrixClient, + mut rx: Receiver, +) { + log::info!( + "WS connected for user {:?} / auth method={}", + client.email, + auth.method.light_str() + ); + + let mut last_heartbeat = Instant::now(); + let mut interval = interval(constants::WS_HEARTBEAT_INTERVAL); + + let reason = loop { + // waits for either `msg_stream` to receive a message from the client, the broadcast channel + // to send a message, or the heartbeat interval timer to tick, yielding the value of + // whichever one is ready first + tokio::select! { + ws_msg = rx.recv() => { + let msg = match ws_msg { + Ok(msg) => msg, + Err(broadcast::error::RecvError::Closed) => break None, + Err(broadcast::error::RecvError::Lagged(_)) => continue, + }; + + match msg { + BroadcastMessage::APITokenDeleted(t) => { + match &auth.method{ + AuthenticatedMethod::Token(tok) if tok.id == t.id => { + log::info!( + "closing WS session of user {:?} as associated token was deleted {:?}", + client.email, + t.base.name + ); + break None; + } + _=>{} + } + + }, + BroadcastMessage::UserDisconnectedFromMatrix(mail) if mail == auth.user.email => { + log::info!( + "closing WS session of user {mail:?} as user was disconnected from Matrix" + ); + break None; + } + + BroadcastMessage::RoomMessageEvent{user, event, room} if user == auth.user.email => { + // Send the message to the websocket + if let Ok(msg) = serde_json::to_string(&WsMessage::RoomMessageEvent { + event:event.content, + room_id: room.room_id().to_owned(), + }) && let Err(e) = session.text(msg).await { + log::error!("Failed to send SyncEvent: {e}"); + } + } + _ => {} + }; + + } + + // heartbeat interval ticked + _tick = interval.tick() => { + // if no heartbeat ping/pong received recently, close the connection + if Instant::now().duration_since(last_heartbeat) > constants::WS_CLIENT_TIMEOUT { + log::info!( + "client has not sent heartbeat in over {:?}; disconnecting",constants::WS_CLIENT_TIMEOUT + ); + + break None; + } + + // send heartbeat ping + let _ = session.ping(b"").await; + }, + + // Websocket messages + msg = msg_stream.next() => { + let msg = match msg { + // received message from WebSocket client + Some(Ok(msg)) => msg, + + // client WebSocket stream error + Some(Err(err)) => { + log::error!("{err}"); + break None; + } + + // client WebSocket stream ended + None => break None + }; + + log::debug!("msg: {msg:?}"); + + match msg { + Message::Text(s) => { + log::info!("Text message from WS: {s}"); + } + + Message::Binary(_) => { + // drop client's binary messages + } + + Message::Close(reason) => { + break reason; + } + + Message::Ping(bytes) => { + last_heartbeat = Instant::now(); + let _ = session.pong(&bytes).await; + } + + Message::Pong(_) => { + last_heartbeat = Instant::now(); + } + + Message::Continuation(_) => { + log::warn!("no support for continuation frames"); + } + + // no-op; ignore + Message::Nop => {} + }; + } + } + }; + + // attempt to close connection gracefully + let _ = session.close(reason).await; + + log::info!("WS disconnected for user {:?}", client.email); +} diff --git a/matrixgw_backend/src/extractors/auth_extractor.rs b/matrixgw_backend/src/extractors/auth_extractor.rs index b3a390f..339ca5d 100644 --- a/matrixgw_backend/src/extractors/auth_extractor.rs +++ b/matrixgw_backend/src/extractors/auth_extractor.rs @@ -28,6 +28,16 @@ pub enum AuthenticatedMethod { Token(APIToken), } +impl AuthenticatedMethod { + pub fn light_str(&self) -> String { + match self { + AuthenticatedMethod::Cookie => "Cookie".to_string(), + AuthenticatedMethod::Dev => "DevAuthentication".to_string(), + AuthenticatedMethod::Token(t) => format!("Token({:?} - {})", t.id, t.base.name), + } + } +} + pub struct AuthExtractor { pub user: User, pub method: AuthenticatedMethod, diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index f873bf8..75a52ef 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -11,7 +11,7 @@ use matrixgw_backend::broadcast_messages::BroadcastMessage; use matrixgw_backend::constants; use matrixgw_backend::controllers::{ auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller, - tokens_controller, + tokens_controller, ws_controller, }; use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor; use matrixgw_backend::users::User; @@ -133,6 +133,7 @@ async fn main() -> std::io::Result<()> { "/api/matrix_sync/status", web::get().to(matrix_sync_thread_controller::status), ) + .service(web::resource("/api/ws").route(web::get().to(ws_controller::ws))) }) .workers(4) .bind(&AppConfig::get().listen_address)? From 3ecfc6b4701195ac8a8e2cf92fb71d03053b4ed1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Nov 2025 19:14:02 +0100 Subject: [PATCH 048/124] Add base debug WS route --- matrixgw_frontend/src/App.tsx | 2 + matrixgw_frontend/src/api/WsApi.ts | 15 ++++++ .../src/routes/MatrixLinkRoute.tsx | 2 +- matrixgw_frontend/src/routes/WSDebugRoute.tsx | 54 +++++++++++++++++++ 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 matrixgw_frontend/src/api/WsApi.ts create mode 100644 matrixgw_frontend/src/routes/WSDebugRoute.tsx diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx index a202bcb..71e9385 100644 --- a/matrixgw_frontend/src/App.tsx +++ b/matrixgw_frontend/src/App.tsx @@ -14,6 +14,7 @@ import { HomeRoute } from "./routes/HomeRoute"; import { MatrixAuthCallback } from "./routes/MatrixAuthCallback"; import { MatrixLinkRoute } from "./routes/MatrixLinkRoute"; import { NotFoundRoute } from "./routes/NotFoundRoute"; +import { WSDebugRoute } from "./routes/WSDebugRoute"; import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage"; @@ -43,6 +44,7 @@ export function App(): React.ReactElement { } /> } /> } /> + } /> } /> ) : ( diff --git a/matrixgw_frontend/src/api/WsApi.ts b/matrixgw_frontend/src/api/WsApi.ts new file mode 100644 index 0000000..8560808 --- /dev/null +++ b/matrixgw_frontend/src/api/WsApi.ts @@ -0,0 +1,15 @@ +import { APIClient } from "./ApiClient"; + +export type WsMessage = { + type: string; + [k: string]: any; +}; + +export class WsApi { + /** + * Get WebSocket URL + */ + static get WsURL(): string { + return APIClient.backendURL() + "/ws"; + } +} diff --git a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx index 82173bf..1c57e5b 100644 --- a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx +++ b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx @@ -256,7 +256,7 @@ function SyncThreadStatus(): React.ReactElement { React.useEffect(() => { const interval = setInterval(loadStatus, 1000); - () => clearInterval(interval); + return () => clearInterval(interval); }, []); return ( diff --git a/matrixgw_frontend/src/routes/WSDebugRoute.tsx b/matrixgw_frontend/src/routes/WSDebugRoute.tsx new file mode 100644 index 0000000..e7179cb --- /dev/null +++ b/matrixgw_frontend/src/routes/WSDebugRoute.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { WsApi, type WsMessage } from "../api/WsApi"; +import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider"; +import { time } from "../utils/DateUtils"; +import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; + +const State = { + Closed: "Closed", + Connected: "Connected", + Error: "Error", +} as const; + +type TimestampedMessages = WsMessage & { time: number }; + +export function WSDebugRoute(): React.ReactElement { + const snackbar = useSnackbar(); + + const [state, setState] = React.useState(State.Closed); + const wsRef = React.useRef(undefined); + + const [messages, setMessages] = React.useState([]); + + React.useEffect(() => { + const ws = new WebSocket(WsApi.WsURL); + wsRef.current = ws; + + ws.onopen = () => setState(State.Connected); + ws.onerror = (e) => { + console.error(`WS Debug error! ${e}`); + snackbar(`WebSocket error! ${e}`); + setState(State.Error); + }; + ws.onclose = () => { + setState(State.Closed); + wsRef.current = undefined; + }; + + ws.onmessage = (msg) => { + const dec = JSON.parse(msg.data); + setMessages((l) => { + return [{ time: time(), ...dec }, ...l]; + }); + }; + + return () => ws.close(); + }, []); + + return ( + + State: {state} + {JSON.stringify(messages)} + + ); +} From 055ab3759c3934fd953a44f9def881ea22bc155b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Nov 2025 19:15:42 +0100 Subject: [PATCH 049/124] Remove frontend messages --- matrixgw_frontend/README.md | 75 +------------------------------------ 1 file changed, 2 insertions(+), 73 deletions(-) diff --git a/matrixgw_frontend/README.md b/matrixgw_frontend/README.md index d2e7761..f5e6f33 100644 --- a/matrixgw_frontend/README.md +++ b/matrixgw_frontend/README.md @@ -1,73 +1,2 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` +# MatrixGW frontend +Built using React + TypeScript + Vite \ No newline at end of file From 7203671b182b2f147734484fbf9481d4715a979e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Nov 2025 19:30:09 +0100 Subject: [PATCH 050/124] Pretty rendering of JSON messages --- matrixgw_frontend/package-lock.json | 13 +++++++++++++ matrixgw_frontend/package.json | 1 + matrixgw_frontend/src/routes/WSDebugRoute.tsx | 16 +++++++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index cf89c4a..e91bab4 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -23,6 +23,7 @@ "qrcode.react": "^4.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-json-view-lite": "^2.5.0", "react-router": "^7.9.5" }, "devDependencies": { @@ -3674,6 +3675,18 @@ "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", "license": "MIT" }, + "node_modules/react-json-view-lite": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", + "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index f9b7daf..4d8f1b0 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -25,6 +25,7 @@ "qrcode.react": "^4.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-json-view-lite": "^2.5.0", "react-router": "^7.9.5" }, "devDependencies": { diff --git a/matrixgw_frontend/src/routes/WSDebugRoute.tsx b/matrixgw_frontend/src/routes/WSDebugRoute.tsx index e7179cb..f7073bf 100644 --- a/matrixgw_frontend/src/routes/WSDebugRoute.tsx +++ b/matrixgw_frontend/src/routes/WSDebugRoute.tsx @@ -1,4 +1,6 @@ import React from "react"; +import { JsonView, darkStyles } from "react-json-view-lite"; +import "react-json-view-lite/dist/index.css"; import { WsApi, type WsMessage } from "../api/WsApi"; import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider"; import { time } from "../utils/DateUtils"; @@ -48,7 +50,19 @@ export function WSDebugRoute(): React.ReactElement { return ( State: {state} - {JSON.stringify(messages)} + {messages.map((msg, id) => ( +
    + level < 2} + style={{ + ...darkStyles, + container: "", + }} + /> +
    + ))}
    ); } From 6b70842b61ed22c2c1b9a14a2d61e4556292bdcf Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 20 Nov 2025 19:31:17 +0100 Subject: [PATCH 051/124] Display state in color --- matrixgw_frontend/src/routes/WSDebugRoute.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/matrixgw_frontend/src/routes/WSDebugRoute.tsx b/matrixgw_frontend/src/routes/WSDebugRoute.tsx index f7073bf..a4e3bfd 100644 --- a/matrixgw_frontend/src/routes/WSDebugRoute.tsx +++ b/matrixgw_frontend/src/routes/WSDebugRoute.tsx @@ -49,7 +49,12 @@ export function WSDebugRoute(): React.ReactElement { return ( - State: {state} +
    + State:{" "} + + {state} + +
    {messages.map((msg, id) => (
    Date: Fri, 21 Nov 2025 09:12:13 +0100 Subject: [PATCH 052/124] Block WS access if Matrix account is not linked --- matrixgw_backend/src/controllers/ws_controller.rs | 7 ++++++- .../src/extractors/matrix_client_extractor.rs | 1 + matrixgw_backend/src/users.rs | 1 + matrixgw_frontend/src/api/AuthApi.ts | 1 + matrixgw_frontend/src/routes/HomeRoute.tsx | 2 +- matrixgw_frontend/src/routes/WSDebugRoute.tsx | 7 ++++++- .../src/widgets/dashboard/DashboardSidebar.tsx | 11 ++++++++++- 7 files changed, 26 insertions(+), 4 deletions(-) diff --git a/matrixgw_backend/src/controllers/ws_controller.rs b/matrixgw_backend/src/controllers/ws_controller.rs index 9ac0092..c9ae359 100644 --- a/matrixgw_backend/src/controllers/ws_controller.rs +++ b/matrixgw_backend/src/controllers/ws_controller.rs @@ -6,7 +6,7 @@ use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use crate::matrix_connection::matrix_client::MatrixClient; use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use actix_web::dev::Payload; -use actix_web::{FromRequest, HttpRequest, web}; +use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; use actix_ws::Message; use futures_util::StreamExt; use matrix_sdk::ruma::OwnedRoomId; @@ -38,6 +38,11 @@ pub async fn ws( // Forcefully ignore request payload by manually extracting authentication information let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?; + // Check if Matrix link has been established first + if !client.client.is_client_connected() { + return Ok(HttpResponse::ExpectationFailed().json("Matrix link not established yet!")); + } + // Ensure sync thread is started ractor::cast!( manager, diff --git a/matrixgw_backend/src/extractors/matrix_client_extractor.rs b/matrixgw_backend/src/extractors/matrix_client_extractor.rs index 0a80c3e..8ba5688 100644 --- a/matrixgw_backend/src/extractors/matrix_client_extractor.rs +++ b/matrixgw_backend/src/extractors/matrix_client_extractor.rs @@ -15,6 +15,7 @@ impl MatrixClientExtractor { pub async fn to_extended_user_info(&self) -> anyhow::Result { Ok(ExtendedUserInfo { user: self.auth.user.clone(), + matrix_account_connected: self.client.is_client_connected(), matrix_user_id: self.client.user_id().map(|id| id.to_string()), matrix_device_id: self.client.device_id().map(|id| id.to_string()), matrix_recovery_state: self.client.recovery_state(), diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs index 26f1f17..6727e0b 100644 --- a/matrixgw_backend/src/users.rs +++ b/matrixgw_backend/src/users.rs @@ -281,6 +281,7 @@ impl APIToken { pub struct ExtendedUserInfo { #[serde(flatten)] pub user: User, + pub matrix_account_connected: bool, pub matrix_user_id: Option, pub matrix_device_id: Option, pub matrix_recovery_state: EncryptionRecoveryState, diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts index c9d0fa8..bcf6f3d 100644 --- a/matrixgw_frontend/src/api/AuthApi.ts +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -6,6 +6,7 @@ export interface UserInfo { time_update: number; name: string; email: string; + matrix_account_connected: boolean; matrix_user_id?: string; matrix_device_id?: string; matrix_recovery_state?: "Enabled" | "Disabled" | "Unknown" | "Incomplete"; diff --git a/matrixgw_frontend/src/routes/HomeRoute.tsx b/matrixgw_frontend/src/routes/HomeRoute.tsx index e404074..829833c 100644 --- a/matrixgw_frontend/src/routes/HomeRoute.tsx +++ b/matrixgw_frontend/src/routes/HomeRoute.tsx @@ -6,7 +6,7 @@ import { NotLinkedAccountMessage } from "../widgets/NotLinkedAccountMessage"; export function HomeRoute(): React.ReactElement { const user = useUserInfo(); - if (!user.info.matrix_user_id) return ; + if (!user.info.matrix_account_connected) return ; return (

    diff --git a/matrixgw_frontend/src/routes/WSDebugRoute.tsx b/matrixgw_frontend/src/routes/WSDebugRoute.tsx index a4e3bfd..fa6fb67 100644 --- a/matrixgw_frontend/src/routes/WSDebugRoute.tsx +++ b/matrixgw_frontend/src/routes/WSDebugRoute.tsx @@ -4,7 +4,9 @@ import "react-json-view-lite/dist/index.css"; import { WsApi, type WsMessage } from "../api/WsApi"; import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider"; import { time } from "../utils/DateUtils"; +import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; +import { NotLinkedAccountMessage } from "../widgets/NotLinkedAccountMessage"; const State = { Closed: "Closed", @@ -15,6 +17,9 @@ const State = { type TimestampedMessages = WsMessage & { time: number }; export function WSDebugRoute(): React.ReactElement { + const user = useUserInfo(); + if (!user.info.matrix_account_connected) return ; + const snackbar = useSnackbar(); const [state, setState] = React.useState(State.Closed); @@ -28,7 +33,7 @@ export function WSDebugRoute(): React.ReactElement { ws.onopen = () => setState(State.Connected); ws.onerror = (e) => { - console.error(`WS Debug error! ${e}`); + console.error(`WS Debug error!`, e); snackbar(`WebSocket error! ${e}`); setState(State.Error); }; diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx index a2c7f3d..7a9974f 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx @@ -8,6 +8,7 @@ import { useTheme } from "@mui/material/styles"; import type {} from "@mui/material/themeCssVarsAugmentation"; import useMediaQuery from "@mui/material/useMediaQuery"; import * as React from "react"; +import { useUserInfo } from "./BaseAuthenticatedPage"; import DashboardSidebarContext from "./DashboardSidebarContext"; import DashboardSidebarDividerItem from "./DashboardSidebarDividerItem"; import DashboardSidebarPageItem from "./DashboardSidebarPageItem"; @@ -31,6 +32,7 @@ export default function DashboardSidebar({ container, }: DashboardSidebarProps) { const theme = useTheme(); + const user = useUserInfo(); const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm")); const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md")); @@ -99,6 +101,7 @@ export default function DashboardSidebar({ }} > } href="/" @@ -115,6 +118,7 @@ export default function DashboardSidebar({ href="/tokens" /> } href="/wsdebug" @@ -123,7 +127,12 @@ export default function DashboardSidebar({ ), - [mini, hasDrawerTransitions, isFullyExpanded] + [ + mini, + hasDrawerTransitions, + isFullyExpanded, + user.info.matrix_account_connected, + ] ); const getDrawerSharedSx = React.useCallback( From 751e3b865470bfd0ca37e1b55e74084077210b41 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Nov 2025 09:35:21 +0100 Subject: [PATCH 053/124] Redact more events --- matrixgw_backend/src/broadcast_messages.rs | 14 ++++++ .../src/controllers/ws_controller.rs | 39 +++++++++++++++- .../src/matrix_connection/matrix_client.rs | 3 ++ .../src/matrix_connection/sync_thread.rs | 46 ++++++++++++++++--- 4 files changed, 94 insertions(+), 8 deletions(-) diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs index b5f9407..5c8b9bd 100644 --- a/matrixgw_backend/src/broadcast_messages.rs +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -1,7 +1,9 @@ use crate::matrix_connection::sync_thread::MatrixSyncTaskID; use crate::users::{APIToken, UserEmail}; use matrix_sdk::Room; +use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent; use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent; +use matrix_sdk::ruma::events::room::redaction::OriginalSyncRoomRedactionEvent; use matrix_sdk::sync::SyncResponse; pub type BroadcastSender = tokio::sync::broadcast::Sender; @@ -23,6 +25,18 @@ pub enum BroadcastMessage { event: Box, room: Room, }, + /// New reaction message + ReactionEvent { + user: UserEmail, + event: Box, + room: Room, + }, + /// New room redaction + RoomRedactionEvent { + user: UserEmail, + event: Box, + room: Room, + }, /// Raw Matrix sync response MatrixSyncResponse { user: UserEmail, sync: SyncResponse }, } diff --git a/matrixgw_backend/src/controllers/ws_controller.rs b/matrixgw_backend/src/controllers/ws_controller.rs index c9ae359..5ed98e2 100644 --- a/matrixgw_backend/src/controllers/ws_controller.rs +++ b/matrixgw_backend/src/controllers/ws_controller.rs @@ -10,7 +10,9 @@ use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; use actix_ws::Message; use futures_util::StreamExt; use matrix_sdk::ruma::OwnedRoomId; +use matrix_sdk::ruma::events::reaction::ReactionEventContent; use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; +use matrix_sdk::ruma::events::room::redaction::RoomRedactionEventContent; use ractor::ActorRef; use std::time::Instant; use tokio::sync::broadcast; @@ -23,7 +25,19 @@ use tokio::time::interval; pub enum WsMessage { /// Room message event RoomMessageEvent { - event: RoomMessageEventContent, + event: Box, + room_id: OwnedRoomId, + }, + + /// Room reaction event + RoomReactionEvent { + event: Box, + room_id: OwnedRoomId, + }, + + /// Room reaction event + RoomRedactionEvent { + event: Box, room_id: OwnedRoomId, }, } @@ -119,12 +133,33 @@ pub async fn ws_handler( BroadcastMessage::RoomMessageEvent{user, event, room} if user == auth.user.email => { // Send the message to the websocket if let Ok(msg) = serde_json::to_string(&WsMessage::RoomMessageEvent { - event:event.content, + event: Box::new(event.content), room_id: room.room_id().to_owned(), }) && let Err(e) = session.text(msg).await { log::error!("Failed to send SyncEvent: {e}"); } } + + BroadcastMessage::ReactionEvent{user, event, room} if user == auth.user.email => { + // Send the message to the websocket + if let Ok(msg) = serde_json::to_string(&WsMessage::RoomReactionEvent { + event: Box::new(event.content), + room_id: room.room_id().to_owned(), + }) && let Err(e) = session.text(msg).await { + log::error!("Failed to send SyncEvent: {e}"); + } + } + + BroadcastMessage::RoomRedactionEvent{user, event, room} if user == auth.user.email => { + // Send the message to the websocket + if let Ok(msg) = serde_json::to_string(&WsMessage::RoomRedactionEvent { + event: Box::new(event.content), + room_id: room.room_id().to_owned(), + }) && let Err(e) = session.text(msg).await { + log::error!("Failed to send SyncEvent: {e}"); + } + } + _ => {} }; diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index eb7702e..488773b 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -167,6 +167,9 @@ impl MatrixClient { .encryption() .wait_for_e2ee_initialization_tasks() .await; + + // Save stored session once + client.save_stored_session().await?; } // Automatically save session when token gets refreshed diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs index 223fbc3..2bfb397 100644 --- a/matrixgw_backend/src/matrix_connection/sync_thread.rs +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -7,7 +7,9 @@ use crate::matrix_connection::matrix_client::MatrixClient; use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use futures_util::StreamExt; use matrix_sdk::Room; +use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent; use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent; +use matrix_sdk::ruma::events::room::redaction::OriginalSyncRoomRedactionEvent; use ractor::ActorRef; #[derive(Clone, Debug, Eq, PartialEq)] @@ -48,19 +50,49 @@ async fn sync_thread_task( let mut sync_stream = client.sync_stream().await; + let mut handlers = vec![]; + let tx_msg_handle = tx.clone(); - let user = client.email.clone(); - let room_message_handle = client.add_event_handler( + let user_msg_handle = client.email.clone(); + handlers.push(client.add_event_handler( async move |event: OriginalSyncRoomMessageEvent, room: Room| { if let Err(e) = tx_msg_handle.send(BroadcastMessage::RoomMessageEvent { - user: user.clone(), + user: user_msg_handle.clone(), event: Box::new(event), room, }) { - log::warn!("Failed to forward room event! {e}"); + log::warn!("Failed to forward room message event! {e}"); } }, - ); + )); + + let tx_reac_handle = tx.clone(); + let user_reac_handle = client.email.clone(); + handlers.push(client.add_event_handler( + async move |event: OriginalSyncReactionEvent, room: Room| { + if let Err(e) = tx_reac_handle.send(BroadcastMessage::ReactionEvent { + user: user_reac_handle.clone(), + event: Box::new(event), + room, + }) { + log::warn!("Failed to forward reaction event! {e}"); + } + }, + )); + + let tx_redac_handle = tx.clone(); + let user_redac_handle = client.email.clone(); + handlers.push(client.add_event_handler( + async move |event: OriginalSyncRoomRedactionEvent, room: Room| { + if let Err(e) = tx_redac_handle.send(BroadcastMessage::RoomRedactionEvent { + user: user_redac_handle.clone(), + event: Box::new(event), + room, + }) { + log::warn!("Failed to forward reaction event! {e}"); + } + }, + )); loop { tokio::select! { @@ -103,7 +135,9 @@ async fn sync_thread_task( } } - client.remove_event_handler(room_message_handle); + for h in handlers { + client.remove_event_handler(h); + } // Notify manager about termination, so this thread can be removed from the list log::info!("Sync thread {id:?} terminated!"); From 8d2cea5f82eb34c27cdd027a025c6ebd7d5d02b9 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Nov 2025 10:30:48 +0100 Subject: [PATCH 054/124] Refactor messages propagation --- matrixgw_backend/Cargo.lock | 1 - matrixgw_backend/Cargo.toml | 3 +- matrixgw_backend/src/broadcast_messages.rs | 25 +++--- .../src/controllers/ws_controller.rs | 82 ++++++++++--------- .../src/matrix_connection/sync_thread.rs | 22 ++--- 5 files changed, 65 insertions(+), 68 deletions(-) diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index 46d938c..9662dae 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -3064,7 +3064,6 @@ dependencies = [ "thiserror 2.0.17", "tokio", "url", - "urlencoding", "uuid", ] diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index f3b32d1..d146f47 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -18,7 +18,6 @@ actix-cors = "0.7.1" light-openid = "1.0.4" bytes = "1.10.1" sha2 = "0.10.9" -urlencoding = "2.1.3" base16ct = { version = "0.3.0", features = ["alloc"] } futures-util = "0.3.31" jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] } @@ -28,7 +27,7 @@ ipnet = { version = "2.11.0", features = ["serde"] } rand = "0.9.2" hex = "0.4.3" mailchecker = "6.0.19" -matrix-sdk = "0.14.0" +matrix-sdk = { version = "0.14.0" } url = "2.5.7" ractor = "0.15.9" serde_json = "1.0.145" diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs index 5c8b9bd..3294449 100644 --- a/matrixgw_backend/src/broadcast_messages.rs +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -8,6 +8,13 @@ use matrix_sdk::sync::SyncResponse; pub type BroadcastSender = tokio::sync::broadcast::Sender; +#[derive(Debug, Clone)] +pub struct BxRoomEvent { + pub user: UserEmail, + pub event: Box, + pub room: Room, +} + /// Broadcast messages #[derive(Debug, Clone)] pub enum BroadcastMessage { @@ -20,23 +27,11 @@ pub enum BroadcastMessage { /// Matrix sync thread has been interrupted SyncThreadStopped(MatrixSyncTaskID), /// New room message - RoomMessageEvent { - user: UserEmail, - event: Box, - room: Room, - }, + RoomMessageEvent(BxRoomEvent), /// New reaction message - ReactionEvent { - user: UserEmail, - event: Box, - room: Room, - }, + ReactionEvent(BxRoomEvent), /// New room redaction - RoomRedactionEvent { - user: UserEmail, - event: Box, - room: Room, - }, + RoomRedactionEvent(BxRoomEvent), /// Raw Matrix sync response MatrixSyncResponse { user: UserEmail, sync: SyncResponse }, } diff --git a/matrixgw_backend/src/controllers/ws_controller.rs b/matrixgw_backend/src/controllers/ws_controller.rs index 5ed98e2..c516737 100644 --- a/matrixgw_backend/src/controllers/ws_controller.rs +++ b/matrixgw_backend/src/controllers/ws_controller.rs @@ -5,6 +5,7 @@ use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use crate::matrix_connection::matrix_client::MatrixClient; use crate::matrix_connection::matrix_manager::MatrixManagerMsg; +use crate::users::UserEmail; use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; use actix_ws::Message; @@ -19,27 +20,50 @@ use tokio::sync::broadcast; use tokio::sync::broadcast::Receiver; use tokio::time::interval; +#[derive(Debug, serde::Serialize)] +pub struct WsRoomEvent { + pub event: Box, + pub room_id: OwnedRoomId, +} + /// Messages sent to the client #[derive(Debug, serde::Serialize)] #[serde(tag = "type")] pub enum WsMessage { /// Room message event - RoomMessageEvent { - event: Box, - room_id: OwnedRoomId, - }, + RoomMessageEvent(WsRoomEvent), /// Room reaction event - RoomReactionEvent { - event: Box, - room_id: OwnedRoomId, - }, + RoomReactionEvent(WsRoomEvent), /// Room reaction event - RoomRedactionEvent { - event: Box, - room_id: OwnedRoomId, - }, + RoomRedactionEvent(WsRoomEvent), +} + +impl WsMessage { + pub fn from_bx_message(msg: &BroadcastMessage, user: &UserEmail) -> Option { + match msg { + BroadcastMessage::RoomMessageEvent(evt) if &evt.user == user => { + Some(Self::RoomMessageEvent(WsRoomEvent { + event: Box::new(evt.event.content.clone()), + room_id: evt.room.room_id().to_owned(), + })) + } + BroadcastMessage::ReactionEvent(evt) if &evt.user == user => { + Some(Self::RoomReactionEvent(WsRoomEvent { + event: Box::new(evt.event.content.clone()), + room_id: evt.room.room_id().to_owned(), + })) + } + BroadcastMessage::RoomRedactionEvent(evt) if &evt.user == user => { + Some(Self::RoomRedactionEvent(WsRoomEvent { + event: Box::new(evt.event.content.clone()), + room_id: evt.room.room_id().to_owned(), + })) + } + _ => None, + } + } } /// Main WS route @@ -108,8 +132,8 @@ pub async fn ws_handler( Err(broadcast::error::RecvError::Lagged(_)) => continue, }; - match msg { - BroadcastMessage::APITokenDeleted(t) => { + match (&msg, WsMessage::from_bx_message(&msg, &auth.user.email)) { + (BroadcastMessage::APITokenDeleted(t), _) => { match &auth.method{ AuthenticatedMethod::Token(tok) if tok.id == t.id => { log::info!( @@ -123,39 +147,17 @@ pub async fn ws_handler( } }, - BroadcastMessage::UserDisconnectedFromMatrix(mail) if mail == auth.user.email => { + (BroadcastMessage::UserDisconnectedFromMatrix(mail), _) if mail == &auth.user.email => { log::info!( "closing WS session of user {mail:?} as user was disconnected from Matrix" ); break None; } - BroadcastMessage::RoomMessageEvent{user, event, room} if user == auth.user.email => { + (_, Some(message)) => { // Send the message to the websocket - if let Ok(msg) = serde_json::to_string(&WsMessage::RoomMessageEvent { - event: Box::new(event.content), - room_id: room.room_id().to_owned(), - }) && let Err(e) = session.text(msg).await { - log::error!("Failed to send SyncEvent: {e}"); - } - } - - BroadcastMessage::ReactionEvent{user, event, room} if user == auth.user.email => { - // Send the message to the websocket - if let Ok(msg) = serde_json::to_string(&WsMessage::RoomReactionEvent { - event: Box::new(event.content), - room_id: room.room_id().to_owned(), - }) && let Err(e) = session.text(msg).await { - log::error!("Failed to send SyncEvent: {e}"); - } - } - - BroadcastMessage::RoomRedactionEvent{user, event, room} if user == auth.user.email => { - // Send the message to the websocket - if let Ok(msg) = serde_json::to_string(&WsMessage::RoomRedactionEvent { - event: Box::new(event.content), - room_id: room.room_id().to_owned(), - }) && let Err(e) = session.text(msg).await { + if let Ok(msg) = serde_json::to_string(&message) + && let Err(e) = session.text(msg).await { log::error!("Failed to send SyncEvent: {e}"); } } diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs index 2bfb397..4a32e61 100644 --- a/matrixgw_backend/src/matrix_connection/sync_thread.rs +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -2,7 +2,7 @@ //! //! This file contains the logic performed by the threads that synchronize with Matrix account. -use crate::broadcast_messages::{BroadcastMessage, BroadcastSender}; +use crate::broadcast_messages::{BroadcastMessage, BroadcastSender, BxRoomEvent}; use crate::matrix_connection::matrix_client::MatrixClient; use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use futures_util::StreamExt; @@ -56,11 +56,11 @@ async fn sync_thread_task( let user_msg_handle = client.email.clone(); handlers.push(client.add_event_handler( async move |event: OriginalSyncRoomMessageEvent, room: Room| { - if let Err(e) = tx_msg_handle.send(BroadcastMessage::RoomMessageEvent { + if let Err(e) = tx_msg_handle.send(BroadcastMessage::RoomMessageEvent(BxRoomEvent { user: user_msg_handle.clone(), event: Box::new(event), room, - }) { + })) { log::warn!("Failed to forward room message event! {e}"); } }, @@ -70,11 +70,11 @@ async fn sync_thread_task( let user_reac_handle = client.email.clone(); handlers.push(client.add_event_handler( async move |event: OriginalSyncReactionEvent, room: Room| { - if let Err(e) = tx_reac_handle.send(BroadcastMessage::ReactionEvent { + if let Err(e) = tx_reac_handle.send(BroadcastMessage::ReactionEvent(BxRoomEvent { user: user_reac_handle.clone(), event: Box::new(event), room, - }) { + })) { log::warn!("Failed to forward reaction event! {e}"); } }, @@ -84,11 +84,13 @@ async fn sync_thread_task( let user_redac_handle = client.email.clone(); handlers.push(client.add_event_handler( async move |event: OriginalSyncRoomRedactionEvent, room: Room| { - if let Err(e) = tx_redac_handle.send(BroadcastMessage::RoomRedactionEvent { - user: user_redac_handle.clone(), - event: Box::new(event), - room, - }) { + if let Err(e) = + tx_redac_handle.send(BroadcastMessage::RoomRedactionEvent(BxRoomEvent { + user: user_redac_handle.clone(), + event: Box::new(event), + room, + })) + { log::warn!("Failed to forward reaction event! {e}"); } }, From 1385afc974685590b53e57cc49749832663decb8 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Nov 2025 11:40:51 +0100 Subject: [PATCH 055/124] Add more information to websocket messages --- matrixgw_backend/src/broadcast_messages.rs | 2 +- .../src/controllers/ws_controller.rs | 22 ++++++++++++++----- .../src/matrix_connection/sync_thread.rs | 6 ++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs index 3294449..db34a75 100644 --- a/matrixgw_backend/src/broadcast_messages.rs +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -11,7 +11,7 @@ pub type BroadcastSender = tokio::sync::broadcast::Sender; #[derive(Debug, Clone)] pub struct BxRoomEvent { pub user: UserEmail, - pub event: Box, + pub data: Box, pub room: Room, } diff --git a/matrixgw_backend/src/controllers/ws_controller.rs b/matrixgw_backend/src/controllers/ws_controller.rs index c516737..0c0fab1 100644 --- a/matrixgw_backend/src/controllers/ws_controller.rs +++ b/matrixgw_backend/src/controllers/ws_controller.rs @@ -10,10 +10,10 @@ use actix_web::dev::Payload; use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; use actix_ws::Message; use futures_util::StreamExt; -use matrix_sdk::ruma::OwnedRoomId; use matrix_sdk::ruma::events::reaction::ReactionEventContent; use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; use matrix_sdk::ruma::events::room::redaction::RoomRedactionEventContent; +use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId}; use ractor::ActorRef; use std::time::Instant; use tokio::sync::broadcast; @@ -22,8 +22,11 @@ use tokio::time::interval; #[derive(Debug, serde::Serialize)] pub struct WsRoomEvent { - pub event: Box, pub room_id: OwnedRoomId, + pub event_id: OwnedEventId, + pub sender: OwnedUserId, + pub origin_server_ts: MilliSecondsSinceUnixEpoch, + pub data: Box, } /// Messages sent to the client @@ -45,20 +48,29 @@ impl WsMessage { match msg { BroadcastMessage::RoomMessageEvent(evt) if &evt.user == user => { Some(Self::RoomMessageEvent(WsRoomEvent { - event: Box::new(evt.event.content.clone()), room_id: evt.room.room_id().to_owned(), + event_id: evt.data.event_id.clone(), + sender: evt.data.sender.clone(), + origin_server_ts: evt.data.origin_server_ts, + data: Box::new(evt.data.content.clone()), })) } BroadcastMessage::ReactionEvent(evt) if &evt.user == user => { Some(Self::RoomReactionEvent(WsRoomEvent { - event: Box::new(evt.event.content.clone()), room_id: evt.room.room_id().to_owned(), + event_id: evt.data.event_id.clone(), + sender: evt.data.sender.clone(), + origin_server_ts: evt.data.origin_server_ts, + data: Box::new(evt.data.content.clone()), })) } BroadcastMessage::RoomRedactionEvent(evt) if &evt.user == user => { Some(Self::RoomRedactionEvent(WsRoomEvent { - event: Box::new(evt.event.content.clone()), room_id: evt.room.room_id().to_owned(), + event_id: evt.data.event_id.clone(), + sender: evt.data.sender.clone(), + origin_server_ts: evt.data.origin_server_ts, + data: Box::new(evt.data.content.clone()), })) } _ => None, diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs index 4a32e61..30f4307 100644 --- a/matrixgw_backend/src/matrix_connection/sync_thread.rs +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -58,7 +58,7 @@ async fn sync_thread_task( async move |event: OriginalSyncRoomMessageEvent, room: Room| { if let Err(e) = tx_msg_handle.send(BroadcastMessage::RoomMessageEvent(BxRoomEvent { user: user_msg_handle.clone(), - event: Box::new(event), + data: Box::new(event), room, })) { log::warn!("Failed to forward room message event! {e}"); @@ -72,7 +72,7 @@ async fn sync_thread_task( async move |event: OriginalSyncReactionEvent, room: Room| { if let Err(e) = tx_reac_handle.send(BroadcastMessage::ReactionEvent(BxRoomEvent { user: user_reac_handle.clone(), - event: Box::new(event), + data: Box::new(event), room, })) { log::warn!("Failed to forward reaction event! {e}"); @@ -87,7 +87,7 @@ async fn sync_thread_task( if let Err(e) = tx_redac_handle.send(BroadcastMessage::RoomRedactionEvent(BxRoomEvent { user: user_redac_handle.clone(), - event: Box::new(event), + data: Box::new(event), room, })) { From ecbe4885c1a1f071b5349ae58013d197515c9032 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Nov 2025 14:52:21 +0100 Subject: [PATCH 056/124] Can get information about rooms --- .../matrix/matrix_room_controller.rs | 58 +++++++++++++++++++ .../src/controllers/matrix/mod.rs | 1 + matrixgw_backend/src/controllers/mod.rs | 1 + matrixgw_backend/src/main.rs | 10 ++++ .../src/matrix_connection/matrix_client.rs | 2 +- 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs create mode 100644 matrixgw_backend/src/controllers/matrix/mod.rs diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs new file mode 100644 index 0000000..f960f78 --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -0,0 +1,58 @@ +use crate::controllers::HttpResult; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use actix_web::{HttpResponse, web}; +use futures_util::{StreamExt, stream}; +use matrix_sdk::ruma::{OwnedRoomId, OwnedUserId}; +use matrix_sdk::{Room, RoomMemberships}; + +#[derive(serde::Serialize)] +pub struct APIRoomInfo { + id: OwnedRoomId, + name: Option, + members: Vec, + has_avatar: bool, +} + +impl APIRoomInfo { + async fn from_room(r: &Room) -> anyhow::Result { + Ok(Self { + id: r.room_id().to_owned(), + name: r.name(), + members: r + .members(RoomMemberships::ACTIVE) + .await? + .into_iter() + .map(|r| r.user_id().to_owned()) + .collect::>(), + has_avatar: r.avatar_url().is_some(), + }) + } +} + +/// Get the list of joined rooms of the user +pub async fn joined_rooms(client: MatrixClientExtractor) -> HttpResult { + let list = stream::iter(client.client.client.joined_rooms()) + .then(async |room| APIRoomInfo::from_room(&room).await) + .collect::>() + .await + .into_iter() + .collect::, _>>()?; + + Ok(HttpResponse::Ok().json(list)) +} + +#[derive(serde::Deserialize)] +pub struct RoomIdInPath { + id: OwnedRoomId, +} + +/// Get the list of joined rooms of the user +pub async fn single_room_info( + client: MatrixClientExtractor, + path: web::Path, +) -> HttpResult { + Ok(match client.client.client.get_room(&path.id) { + None => HttpResponse::NotFound().json("Room not found"), + Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r).await?), + }) +} diff --git a/matrixgw_backend/src/controllers/matrix/mod.rs b/matrixgw_backend/src/controllers/matrix/mod.rs new file mode 100644 index 0000000..6742d04 --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/mod.rs @@ -0,0 +1 @@ +pub mod matrix_room_controller; diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index f457c29..0384002 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -3,6 +3,7 @@ use actix_web::{HttpResponse, ResponseError}; use std::error::Error; pub mod auth_controller; +pub mod matrix; pub mod matrix_link_controller; pub mod matrix_sync_thread_controller; pub mod server_controller; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 75a52ef..fc9c805 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -9,6 +9,7 @@ use actix_web::{App, HttpServer, web}; use matrixgw_backend::app_config::AppConfig; use matrixgw_backend::broadcast_messages::BroadcastMessage; use matrixgw_backend::constants; +use matrixgw_backend::controllers::matrix::matrix_room_controller; use matrixgw_backend::controllers::{ auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller, tokens_controller, ws_controller, @@ -134,6 +135,15 @@ async fn main() -> std::io::Result<()> { web::get().to(matrix_sync_thread_controller::status), ) .service(web::resource("/api/ws").route(web::get().to(ws_controller::ws))) + // Matrix room controller + .route( + "/api/matrix/room/joined", + web::get().to(matrix_room_controller::joined_rooms), + ) + .route( + "/api/matrix/room/{id}", + web::get().to(matrix_room_controller::single_room_info), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/matrix_connection/matrix_client.rs b/matrixgw_backend/src/matrix_connection/matrix_client.rs index 488773b..4ec021a 100644 --- a/matrixgw_backend/src/matrix_connection/matrix_client.rs +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -91,7 +91,7 @@ pub struct FinishMatrixAuth { pub struct MatrixClient { manager: ActorRef, pub email: UserEmail, - client: Client, + pub client: Client, } impl MatrixClient { From e8ce97eea0ef8b0b018fc44b5c89ed4a584640f4 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Nov 2025 15:43:15 +0100 Subject: [PATCH 057/124] Can get room avatar --- matrixgw_backend/Cargo.lock | 21 +++++++ matrixgw_backend/Cargo.toml | 3 +- .../matrix/matrix_room_controller.rs | 20 ++++++- .../controllers/matrix/media_controller.rs | 57 +++++++++++++++++++ .../src/controllers/matrix/mod.rs | 1 + matrixgw_backend/src/controllers/mod.rs | 2 + matrixgw_backend/src/main.rs | 4 ++ matrixgw_backend/src/utils/crypt_utils.rs | 7 ++- 8 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 matrixgw_backend/src/controllers/matrix/media_controller.rs diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index 9662dae..f44d616 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -777,6 +777,17 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -2357,6 +2368,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + [[package]] name = "inout" version = "0.1.4" @@ -3048,6 +3068,7 @@ dependencies = [ "env_logger", "futures-util", "hex", + "infer", "ipnet", "jwt-simple", "lazy-regex", diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index d146f47..84f0947 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -32,4 +32,5 @@ url = "2.5.7" ractor = "0.15.9" serde_json = "1.0.145" lazy-regex = "3.4.2" -actix-ws = "0.3.0" \ No newline at end of file +actix-ws = "0.3.0" +infer = "0.19.0" \ No newline at end of file diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index f960f78..cc6c22b 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -1,6 +1,7 @@ use crate::controllers::HttpResult; +use crate::controllers::matrix::media_controller; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; -use actix_web::{HttpResponse, web}; +use actix_web::{HttpRequest, HttpResponse, web}; use futures_util::{StreamExt, stream}; use matrix_sdk::ruma::{OwnedRoomId, OwnedUserId}; use matrix_sdk::{Room, RoomMemberships}; @@ -56,3 +57,20 @@ pub async fn single_room_info( Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r).await?), }) } + +/// Get room avatar +pub async fn room_avatar( + req: HttpRequest, + client: MatrixClientExtractor, + path: web::Path, +) -> HttpResult { + let Some(room) = client.client.client.get_room(&path.id) else { + return Ok(HttpResponse::NotFound().json("Room not found")); + }; + + let Some(uri) = room.avatar_url() else { + return Ok(HttpResponse::NotFound().json("Room has no avatar")); + }; + + media_controller::serve_media(req, uri).await +} diff --git a/matrixgw_backend/src/controllers/matrix/media_controller.rs b/matrixgw_backend/src/controllers/matrix/media_controller.rs new file mode 100644 index 0000000..781478c --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/media_controller.rs @@ -0,0 +1,57 @@ +use crate::controllers::HttpResult; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use crate::utils::crypt_utils::sha512; +use actix_web::dev::Payload; +use actix_web::http::header; +use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; +use matrix_sdk::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}; +use matrix_sdk::ruma::events::room::MediaSource; +use matrix_sdk::ruma::{OwnedMxcUri, UInt}; + +#[derive(serde::Deserialize)] +struct MediaQuery { + #[serde(default)] + thumbnail: bool, +} + +/// Serve a media file +pub async fn serve_media(req: HttpRequest, media: OwnedMxcUri) -> HttpResult { + let query = web::Query::::from_request(&req, &mut Payload::None).await?; + let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?; + + let media = client + .client + .client + .media() + .get_media_content( + &MediaRequestParameters { + source: MediaSource::Plain(media), + format: match query.thumbnail { + true => MediaFormat::Thumbnail(MediaThumbnailSettings::new( + UInt::new(100).unwrap(), + UInt::new(100).unwrap(), + )), + false => MediaFormat::File, + }, + }, + true, + ) + .await?; + + let digest = sha512(&media); + + let mime_type = infer::get(&media).map(|x| x.mime_type()); + + // Check if the browser already knows the etag + if let Some(c) = req.headers().get(header::IF_NONE_MATCH) + && c.to_str().unwrap_or("") == digest + { + return Ok(HttpResponse::NotModified().finish()); + } + + Ok(HttpResponse::Ok() + .content_type(mime_type.unwrap_or("application/octet-stream")) + .insert_header(("etag", digest)) + .insert_header(("cache-control", "max-age=360000")) + .body(media)) +} diff --git a/matrixgw_backend/src/controllers/matrix/mod.rs b/matrixgw_backend/src/controllers/matrix/mod.rs index 6742d04..5ce5ba4 100644 --- a/matrixgw_backend/src/controllers/matrix/mod.rs +++ b/matrixgw_backend/src/controllers/matrix/mod.rs @@ -1 +1,2 @@ pub mod matrix_room_controller; +pub mod media_controller; diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 0384002..cab029c 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -22,6 +22,8 @@ pub enum HttpFailure { InternalError(#[from] anyhow::Error), #[error("Actix web error: {0}")] ActixError(#[from] actix_web::Error), + #[error("Matrix error: {0}")] + MatrixError(#[from] matrix_sdk::Error), } impl ResponseError for HttpFailure { diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index fc9c805..321f59c 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -144,6 +144,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/{id}", web::get().to(matrix_room_controller::single_room_info), ) + .route( + "/api/matrix/room/{id}/avatar", + web::get().to(matrix_room_controller::room_avatar), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_backend/src/utils/crypt_utils.rs b/matrixgw_backend/src/utils/crypt_utils.rs index 9a22fa9..f1cff8c 100644 --- a/matrixgw_backend/src/utils/crypt_utils.rs +++ b/matrixgw_backend/src/utils/crypt_utils.rs @@ -1,6 +1,11 @@ -use sha2::{Digest, Sha256}; +use sha2::{Digest, Sha256, Sha512}; /// Compute SHA256sum of a given string pub fn sha256str(input: &str) -> String { hex::encode(Sha256::digest(input.as_bytes())) } + +/// Compute SHA256sum of a given byte array +pub fn sha512(input: &[u8]) -> String { + hex::encode(Sha512::digest(input)) +} From b744265242616414c1b356d9a849664c9e99fab8 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Nov 2025 17:14:23 +0100 Subject: [PATCH 058/124] Can get single profile information --- .../matrix/matrix_profile_controller.rs | 40 +++++++++++++++++++ .../matrix/matrix_room_controller.rs | 6 +-- .../src/controllers/matrix/mod.rs | 1 + matrixgw_backend/src/main.rs | 7 +++- 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs diff --git a/matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs new file mode 100644 index 0000000..c0d6935 --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs @@ -0,0 +1,40 @@ +use crate::controllers::HttpResult; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use actix_web::{HttpResponse, web}; +use matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName, get_profile}; +use matrix_sdk::ruma::{OwnedMxcUri, OwnedUserId}; + +#[derive(serde::Deserialize)] +pub struct UserIDInPath { + user_id: OwnedUserId, +} + +#[derive(serde::Serialize)] +struct ProfileResponse { + display_name: Option, + avatar: Option, +} + +impl ProfileResponse { + pub fn from(r: get_profile::v3::Response) -> anyhow::Result { + Ok(Self { + display_name: r.get_static::()?, + avatar: r.get_static::()?, + }) + } +} + +/// Get user profile +pub async fn get_profile( + client: MatrixClientExtractor, + path: web::Path, +) -> HttpResult { + let profile = client + .client + .client + .account() + .fetch_user_profile_of(&path.user_id) + .await?; + + Ok(HttpResponse::Ok().json(ProfileResponse::from(profile)?)) +} diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index cc6c22b..927fe6b 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -3,7 +3,7 @@ use crate::controllers::matrix::media_controller; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use actix_web::{HttpRequest, HttpResponse, web}; use futures_util::{StreamExt, stream}; -use matrix_sdk::ruma::{OwnedRoomId, OwnedUserId}; +use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId}; use matrix_sdk::{Room, RoomMemberships}; #[derive(serde::Serialize)] @@ -11,7 +11,7 @@ pub struct APIRoomInfo { id: OwnedRoomId, name: Option, members: Vec, - has_avatar: bool, + avatar: Option, } impl APIRoomInfo { @@ -25,7 +25,7 @@ impl APIRoomInfo { .into_iter() .map(|r| r.user_id().to_owned()) .collect::>(), - has_avatar: r.avatar_url().is_some(), + avatar: r.avatar_url(), }) } } diff --git a/matrixgw_backend/src/controllers/matrix/mod.rs b/matrixgw_backend/src/controllers/matrix/mod.rs index 5ce5ba4..8c43048 100644 --- a/matrixgw_backend/src/controllers/matrix/mod.rs +++ b/matrixgw_backend/src/controllers/matrix/mod.rs @@ -1,2 +1,3 @@ +pub mod matrix_profile_controller; pub mod matrix_room_controller; pub mod media_controller; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 321f59c..4d1939a 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -9,7 +9,7 @@ use actix_web::{App, HttpServer, web}; use matrixgw_backend::app_config::AppConfig; use matrixgw_backend::broadcast_messages::BroadcastMessage; use matrixgw_backend::constants; -use matrixgw_backend::controllers::matrix::matrix_room_controller; +use matrixgw_backend::controllers::matrix::{matrix_profile_controller, matrix_room_controller}; use matrixgw_backend::controllers::{ auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller, tokens_controller, ws_controller, @@ -148,6 +148,11 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/{id}/avatar", web::get().to(matrix_room_controller::room_avatar), ) + // Matrix profile controller + .route( + "/api/matrix/profile/{user_id}", + web::get().to(matrix_profile_controller::get_profile), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? From 934e6a4cc191060c8bebc9ef34e0940301af88d0 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Nov 2025 17:49:41 +0100 Subject: [PATCH 059/124] Can get multiple profiles information --- .../matrix/matrix_profile_controller.rs | 31 +++++++++++++++++-- matrixgw_backend/src/main.rs | 4 +++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs index c0d6935..a5cea9e 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs @@ -1,6 +1,7 @@ use crate::controllers::HttpResult; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use actix_web::{HttpResponse, web}; +use futures_util::{StreamExt, stream}; use matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName, get_profile}; use matrix_sdk::ruma::{OwnedMxcUri, OwnedUserId}; @@ -11,13 +12,15 @@ pub struct UserIDInPath { #[derive(serde::Serialize)] struct ProfileResponse { + user_id: OwnedUserId, display_name: Option, avatar: Option, } impl ProfileResponse { - pub fn from(r: get_profile::v3::Response) -> anyhow::Result { + pub fn from(user_id: OwnedUserId, r: get_profile::v3::Response) -> anyhow::Result { Ok(Self { + user_id, display_name: r.get_static::()?, avatar: r.get_static::()?, }) @@ -36,5 +39,29 @@ pub async fn get_profile( .fetch_user_profile_of(&path.user_id) .await?; - Ok(HttpResponse::Ok().json(ProfileResponse::from(profile)?)) + Ok(HttpResponse::Ok().json(ProfileResponse::from(path.user_id.clone(), profile)?)) +} + +/// Get multiple users profiles +pub async fn get_multiple(client: MatrixClientExtractor) -> HttpResult { + let users = client.auth.decode_json_body::>()?; + + let list = stream::iter(users) + .then(async |user_id| { + client + .client + .client + .account() + .fetch_user_profile_of(&user_id) + .await + .map(|r| ProfileResponse::from(user_id, r)) + }) + .collect::>() + .await + .into_iter() + .collect::, _>>()? + .into_iter() + .collect::, _>>()?; + + Ok(HttpResponse::Ok().json(list)) } diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 4d1939a..64810f0 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -153,6 +153,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix/profile/{user_id}", web::get().to(matrix_profile_controller::get_profile), ) + .route( + "/api/matrix/profile/get_multiple", + web::post().to(matrix_profile_controller::get_multiple), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? From 35b53fee5c6fd3480b7412523b188fd240d7e332 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Nov 2025 17:55:09 +0100 Subject: [PATCH 060/124] Can request any media file --- ...{media_controller.rs => matrix_media_controller.rs} | 10 ++++++++++ .../src/controllers/matrix/matrix_room_controller.rs | 4 ++-- matrixgw_backend/src/controllers/matrix/mod.rs | 2 +- matrixgw_backend/src/main.rs | 9 ++++++++- 4 files changed, 21 insertions(+), 4 deletions(-) rename matrixgw_backend/src/controllers/matrix/{media_controller.rs => matrix_media_controller.rs} (87%) diff --git a/matrixgw_backend/src/controllers/matrix/media_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs similarity index 87% rename from matrixgw_backend/src/controllers/matrix/media_controller.rs rename to matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs index 781478c..51c485f 100644 --- a/matrixgw_backend/src/controllers/matrix/media_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs @@ -55,3 +55,13 @@ pub async fn serve_media(req: HttpRequest, media: OwnedMxcUri) -> HttpResult { .insert_header(("cache-control", "max-age=360000")) .body(media)) } + +#[derive(serde::Deserialize)] +pub struct MediaMXCInPath { + mxc: OwnedMxcUri, +} + +/// Save media resource handler +pub async fn serve_media_res(req: HttpRequest, media: web::Path) -> HttpResult { + serve_media(req, media.into_inner().mxc).await +} diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index 927fe6b..67295a7 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -1,5 +1,5 @@ use crate::controllers::HttpResult; -use crate::controllers::matrix::media_controller; +use crate::controllers::matrix::matrix_media_controller; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use actix_web::{HttpRequest, HttpResponse, web}; use futures_util::{StreamExt, stream}; @@ -72,5 +72,5 @@ pub async fn room_avatar( return Ok(HttpResponse::NotFound().json("Room has no avatar")); }; - media_controller::serve_media(req, uri).await + matrix_media_controller::serve_media(req, uri).await } diff --git a/matrixgw_backend/src/controllers/matrix/mod.rs b/matrixgw_backend/src/controllers/matrix/mod.rs index 8c43048..2d86649 100644 --- a/matrixgw_backend/src/controllers/matrix/mod.rs +++ b/matrixgw_backend/src/controllers/matrix/mod.rs @@ -1,3 +1,3 @@ +pub mod matrix_media_controller; pub mod matrix_profile_controller; pub mod matrix_room_controller; -pub mod media_controller; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 64810f0..2a88c8c 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -9,7 +9,9 @@ use actix_web::{App, HttpServer, web}; use matrixgw_backend::app_config::AppConfig; use matrixgw_backend::broadcast_messages::BroadcastMessage; use matrixgw_backend::constants; -use matrixgw_backend::controllers::matrix::{matrix_profile_controller, matrix_room_controller}; +use matrixgw_backend::controllers::matrix::{ + matrix_media_controller, matrix_profile_controller, matrix_room_controller, +}; use matrixgw_backend::controllers::{ auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller, tokens_controller, ws_controller, @@ -157,6 +159,11 @@ async fn main() -> std::io::Result<()> { "/api/matrix/profile/get_multiple", web::post().to(matrix_profile_controller::get_multiple), ) + // Matrix media controller + .route( + "/api/matrix/media/{mxc}", + web::get().to(matrix_media_controller::serve_media_res), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? From d23190f9d2729858ac834efbcba7df6d8df8ac74 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 21 Nov 2025 18:38:12 +0100 Subject: [PATCH 061/124] Can get spaces of user --- .../matrix/matrix_room_controller.rs | 33 +++++++++++++++++++ matrixgw_backend/src/main.rs | 4 +++ 2 files changed, 37 insertions(+) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index 67295a7..4344877 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -3,6 +3,7 @@ use crate::controllers::matrix::matrix_media_controller; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use actix_web::{HttpRequest, HttpResponse, web}; use futures_util::{StreamExt, stream}; +use matrix_sdk::room::ParentSpace; use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId}; use matrix_sdk::{Room, RoomMemberships}; @@ -12,10 +13,28 @@ pub struct APIRoomInfo { name: Option, members: Vec, avatar: Option, + is_space: bool, + parents: Vec, } impl APIRoomInfo { async fn from_room(r: &Room) -> anyhow::Result { + let parent_spaces = r + .parent_spaces() + .await? + .collect::>() + .await + .into_iter() + .collect::, _>>()? + .into_iter() + .filter_map(|d| match d { + ParentSpace::Reciprocal(r) | ParentSpace::WithPowerlevel(r) => { + Some(r.room_id().to_owned()) + } + _ => None, + }) + .collect::>(); + Ok(Self { id: r.room_id().to_owned(), name: r.name(), @@ -26,6 +45,8 @@ impl APIRoomInfo { .map(|r| r.user_id().to_owned()) .collect::>(), avatar: r.avatar_url(), + is_space: r.is_space(), + parents: parent_spaces, }) } } @@ -42,6 +63,18 @@ pub async fn joined_rooms(client: MatrixClientExtractor) -> HttpResult { Ok(HttpResponse::Ok().json(list)) } +/// Get joined spaces rooms of user +pub async fn get_joined_spaces(client: MatrixClientExtractor) -> HttpResult { + let list = stream::iter(client.client.client.joined_space_rooms()) + .then(async |room| APIRoomInfo::from_room(&room).await) + .collect::>() + .await + .into_iter() + .collect::, _>>()?; + + Ok(HttpResponse::Ok().json(list)) +} + #[derive(serde::Deserialize)] pub struct RoomIdInPath { id: OwnedRoomId, diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 2a88c8c..6aa4e83 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -142,6 +142,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/joined", web::get().to(matrix_room_controller::joined_rooms), ) + .route( + "/api/matrix/room/joined_spaces", + web::get().to(matrix_room_controller::get_joined_spaces), + ) .route( "/api/matrix/room/{id}", web::get().to(matrix_room_controller::single_room_info), From 7562a7fc612f807fd79fbb4ab306caff50807f03 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 11:20:20 +0100 Subject: [PATCH 062/124] Get latest message for a room --- .../matrix/matrix_event_controller.rs | 60 +++++++++++++++++++ .../matrix/matrix_room_controller.rs | 6 ++ .../src/controllers/matrix/mod.rs | 1 + 3 files changed, 67 insertions(+) create mode 100644 matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs new file mode 100644 index 0000000..a8228de --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -0,0 +1,60 @@ +use futures_util::{StreamExt, stream}; +use matrix_sdk::Room; +use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind}; +use matrix_sdk::room::MessagesOptions; +use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt}; +use serde::Serialize; +use serde_json::value::RawValue; + +#[derive(Serialize)] +pub struct APIEvent { + id: OwnedEventId, + time: MilliSecondsSinceUnixEpoch, + sender: OwnedUserId, + data: Box, +} + +impl APIEvent { + pub async fn from_evt(msg: TimelineEvent, room_id: &RoomId) -> anyhow::Result { + let (event, raw) = match &msg.kind { + TimelineEventKind::Decrypted(d) => (d.event.deserialize()?, d.event.json()), + TimelineEventKind::UnableToDecrypt { event, .. } + | TimelineEventKind::PlainText { event } => ( + event.deserialize()?.into_full_event(room_id.to_owned()), + event.json(), + ), + }; + + Ok(Self { + id: event.event_id().to_owned(), + time: event.origin_server_ts(), + sender: event.sender().to_owned(), + data: raw.to_owned(), + }) + } +} + +#[derive(Serialize)] +pub struct APIEventsList { + pub start: String, + pub end: Option, + pub messages: Vec, +} + +/// Get messages for a given room +pub(super) async fn get_events(room: &Room, limit: u32) -> anyhow::Result { + let mut msg_opts = MessagesOptions::backward(); + msg_opts.limit = UInt::from(limit); + + let messages = room.messages(msg_opts).await?; + Ok(APIEventsList { + start: messages.start, + end: messages.end, + messages: stream::iter(messages.chunk) + .then(async |msg| APIEvent::from_evt(msg, room.room_id()).await) + .collect::>() + .await + .into_iter() + .collect::, _>>()?, + }) +} diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index 4344877..3970c7d 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -1,4 +1,5 @@ use crate::controllers::HttpResult; +use crate::controllers::matrix::matrix_event_controller::{APIEvent, get_events}; use crate::controllers::matrix::matrix_media_controller; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use actix_web::{HttpRequest, HttpResponse, web}; @@ -15,10 +16,13 @@ pub struct APIRoomInfo { avatar: Option, is_space: bool, parents: Vec, + number_unread_messages: u64, + latest_event: Option, } impl APIRoomInfo { async fn from_room(r: &Room) -> anyhow::Result { + // Get parent spaces let parent_spaces = r .parent_spaces() .await? @@ -47,6 +51,8 @@ impl APIRoomInfo { avatar: r.avatar_url(), is_space: r.is_space(), parents: parent_spaces, + number_unread_messages: r.num_unread_messages(), + latest_event: get_events(r, 1).await?.messages.into_iter().next(), }) } } diff --git a/matrixgw_backend/src/controllers/matrix/mod.rs b/matrixgw_backend/src/controllers/matrix/mod.rs index 2d86649..3a34b16 100644 --- a/matrixgw_backend/src/controllers/matrix/mod.rs +++ b/matrixgw_backend/src/controllers/matrix/mod.rs @@ -1,3 +1,4 @@ +pub mod matrix_event_controller; pub mod matrix_media_controller; pub mod matrix_profile_controller; pub mod matrix_room_controller; From bf119a34fb1fb6bed5656830c92cc49d8f29d2a0 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 12:36:59 +0100 Subject: [PATCH 063/124] Can get room messages --- .../matrix/matrix_event_controller.rs | 35 +++++++++++++++++-- .../matrix/matrix_room_controller.rs | 4 +-- matrixgw_backend/src/main.rs | 8 ++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index a8228de..e9732a6 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -1,9 +1,13 @@ +use crate::controllers::HttpResult; +use crate::controllers::matrix::matrix_room_controller::RoomIdInPath; +use crate::extractors::matrix_client_extractor::MatrixClientExtractor; +use actix_web::{HttpResponse, web}; use futures_util::{StreamExt, stream}; use matrix_sdk::Room; use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind}; use matrix_sdk::room::MessagesOptions; use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; #[derive(Serialize)] @@ -42,8 +46,13 @@ pub struct APIEventsList { } /// Get messages for a given room -pub(super) async fn get_events(room: &Room, limit: u32) -> anyhow::Result { +pub(super) async fn get_events( + room: &Room, + limit: u32, + from: Option<&str>, +) -> anyhow::Result { let mut msg_opts = MessagesOptions::backward(); + msg_opts.from = from.map(str::to_string); msg_opts.limit = UInt::from(limit); let messages = room.messages(msg_opts).await?; @@ -58,3 +67,25 @@ pub(super) async fn get_events(room: &Room, limit: u32) -> anyhow::Result, _>>()?, }) } + +#[derive(Deserialize)] +pub struct GetRoomEventsQuery { + #[serde(default)] + limit: Option, + #[serde(default)] + from: Option, +} + +/// Get the events for a room +pub async fn get_for_room( + client: MatrixClientExtractor, + path: web::Path, + query: web::Query, +) -> HttpResult { + let Some(room) = client.client.client.get_room(&path.id) else { + return Ok(HttpResponse::NotFound().json("Room not found!")); + }; + + Ok(HttpResponse::Ok() + .json(get_events(&room, query.limit.unwrap_or(500), query.from.as_deref()).await?)) +} diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index 3970c7d..e2384b5 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -52,7 +52,7 @@ impl APIRoomInfo { is_space: r.is_space(), parents: parent_spaces, number_unread_messages: r.num_unread_messages(), - latest_event: get_events(r, 1).await?.messages.into_iter().next(), + latest_event: get_events(r, 1, None).await?.messages.into_iter().next(), }) } } @@ -83,7 +83,7 @@ pub async fn get_joined_spaces(client: MatrixClientExtractor) -> HttpResult { #[derive(serde::Deserialize)] pub struct RoomIdInPath { - id: OwnedRoomId, + pub(crate) id: OwnedRoomId, } /// Get the list of joined rooms of the user diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 6aa4e83..4bcf520 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -10,7 +10,8 @@ use matrixgw_backend::app_config::AppConfig; use matrixgw_backend::broadcast_messages::BroadcastMessage; use matrixgw_backend::constants; use matrixgw_backend::controllers::matrix::{ - matrix_media_controller, matrix_profile_controller, matrix_room_controller, + matrix_event_controller, matrix_media_controller, matrix_profile_controller, + matrix_room_controller, }; use matrixgw_backend::controllers::{ auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller, @@ -163,6 +164,11 @@ async fn main() -> std::io::Result<()> { "/api/matrix/profile/get_multiple", web::post().to(matrix_profile_controller::get_multiple), ) + // Matrix events controller + .route( + "/api/matrix/room/{id}/events", + web::get().to(matrix_event_controller::get_for_room), + ) // Matrix media controller .route( "/api/matrix/media/{mxc}", From 639cc6c7371fdd0e4990da23425a0fbc29fb889b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 12:54:59 +0100 Subject: [PATCH 064/124] Can send text message --- .../matrix/matrix_event_controller.rs | 22 +++++++++++++++++++ matrixgw_backend/src/main.rs | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index e9732a6..1840549 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -6,6 +6,7 @@ use futures_util::{StreamExt, stream}; use matrix_sdk::Room; use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind}; use matrix_sdk::room::MessagesOptions; +use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt}; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; @@ -89,3 +90,24 @@ pub async fn get_for_room( Ok(HttpResponse::Ok() .json(get_events(&room, query.limit.unwrap_or(500), query.from.as_deref()).await?)) } + +#[derive(Deserialize)] +struct SendTextMessageRequest { + content: String, +} + +pub async fn send_text_message( + client: MatrixClientExtractor, + path: web::Path, +) -> HttpResult { + let req = client.auth.decode_json_body::()?; + + let Some(room) = client.client.client.get_room(&path.id) else { + return Ok(HttpResponse::NotFound().json("Room not found!")); + }; + + room.send(RoomMessageEventContent::text_plain(req.content)) + .await?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 4bcf520..2f7f011 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -169,6 +169,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/{id}/events", web::get().to(matrix_event_controller::get_for_room), ) + .route( + "/api/matrix/room/{id}/send_text_message", + web::post().to(matrix_event_controller::send_text_message), + ) // Matrix media controller .route( "/api/matrix/media/{mxc}", From 0a395b0d265c03b84b2f6cfb1005259944dddfb9 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 13:06:31 +0100 Subject: [PATCH 065/124] Can redact message --- .../matrix/matrix_event_controller.rs | 28 +++++++++++++++++-- .../matrix/matrix_room_controller.rs | 6 ++-- matrixgw_backend/src/main.rs | 12 +++++--- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index 1840549..3e44a37 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -83,7 +83,7 @@ pub async fn get_for_room( path: web::Path, query: web::Query, ) -> HttpResult { - let Some(room) = client.client.client.get_room(&path.id) else { + let Some(room) = client.client.client.get_room(&path.room_id) else { return Ok(HttpResponse::NotFound().json("Room not found!")); }; @@ -102,7 +102,7 @@ pub async fn send_text_message( ) -> HttpResult { let req = client.auth.decode_json_body::()?; - let Some(room) = client.client.client.get_room(&path.id) else { + let Some(room) = client.client.client.get_room(&path.room_id) else { return Ok(HttpResponse::NotFound().json("Room not found!")); }; @@ -111,3 +111,27 @@ pub async fn send_text_message( Ok(HttpResponse::Accepted().finish()) } + +#[derive(serde::Deserialize)] +pub struct EventIdInPath { + pub(crate) event_id: OwnedEventId, +} + +pub async fn redact_event( + client: MatrixClientExtractor, + path: web::Path, + event_path: web::Path, +) -> HttpResult { + let Some(room) = client.client.client.get_room(&path.room_id) else { + return Ok(HttpResponse::NotFound().json("Room not found!")); + }; + + Ok(match room.redact(&event_path.event_id, None, None).await { + Ok(_) => HttpResponse::Accepted().finish(), + + Err(e) => { + log::error!("Failed to redact event {}: {e}", event_path.event_id); + HttpResponse::InternalServerError().json(format!("Failed to redact event! {e}")) + } + }) +} diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index e2384b5..0751f92 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -83,7 +83,7 @@ pub async fn get_joined_spaces(client: MatrixClientExtractor) -> HttpResult { #[derive(serde::Deserialize)] pub struct RoomIdInPath { - pub(crate) id: OwnedRoomId, + pub(crate) room_id: OwnedRoomId, } /// Get the list of joined rooms of the user @@ -91,7 +91,7 @@ pub async fn single_room_info( client: MatrixClientExtractor, path: web::Path, ) -> HttpResult { - Ok(match client.client.client.get_room(&path.id) { + Ok(match client.client.client.get_room(&path.room_id) { None => HttpResponse::NotFound().json("Room not found"), Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r).await?), }) @@ -103,7 +103,7 @@ pub async fn room_avatar( client: MatrixClientExtractor, path: web::Path, ) -> HttpResult { - let Some(room) = client.client.client.get_room(&path.id) else { + let Some(room) = client.client.client.get_room(&path.room_id) else { return Ok(HttpResponse::NotFound().json("Room not found")); }; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 2f7f011..09ae619 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -148,11 +148,11 @@ async fn main() -> std::io::Result<()> { web::get().to(matrix_room_controller::get_joined_spaces), ) .route( - "/api/matrix/room/{id}", + "/api/matrix/room/{room_id}", web::get().to(matrix_room_controller::single_room_info), ) .route( - "/api/matrix/room/{id}/avatar", + "/api/matrix/room/{room_id}/avatar", web::get().to(matrix_room_controller::room_avatar), ) // Matrix profile controller @@ -166,13 +166,17 @@ async fn main() -> std::io::Result<()> { ) // Matrix events controller .route( - "/api/matrix/room/{id}/events", + "/api/matrix/room/{room_id}/events", web::get().to(matrix_event_controller::get_for_room), ) .route( - "/api/matrix/room/{id}/send_text_message", + "/api/matrix/room/{room_id}/send_text_message", web::post().to(matrix_event_controller::send_text_message), ) + .route( + "/api/matrix/room/{room_id}/event/{event_id}", + web::delete().to(matrix_event_controller::redact_event), + ) // Matrix media controller .route( "/api/matrix/media/{mxc}", From 4d72644a31d2b462f8e72c4e45f3d0ceaf214a61 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 13:18:23 +0100 Subject: [PATCH 066/124] Can edit message --- .../matrix/matrix_event_controller.rs | 45 ++++++++++++++++++- matrixgw_backend/src/main.rs | 4 ++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index 3e44a37..7befb98 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -6,7 +6,10 @@ use futures_util::{StreamExt, stream}; use matrix_sdk::Room; use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind}; use matrix_sdk::room::MessagesOptions; -use matrix_sdk::ruma::events::room::message::RoomMessageEventContent; +use matrix_sdk::room::edit::EditedContent; +use matrix_sdk::ruma::events::room::message::{ + RoomMessageEventContent, RoomMessageEventContentWithoutRelation, +}; use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt}; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; @@ -117,6 +120,46 @@ pub struct EventIdInPath { pub(crate) event_id: OwnedEventId, } +pub async fn set_text_content( + client: MatrixClientExtractor, + path: web::Path, + event_path: web::Path, +) -> HttpResult { + let req = client.auth.decode_json_body::()?; + + let Some(room) = client.client.client.get_room(&path.room_id) else { + return Ok(HttpResponse::NotFound().json("Room not found!")); + }; + + let edit_event = match room + .make_edit_event( + &event_path.event_id, + EditedContent::RoomMessage(RoomMessageEventContentWithoutRelation::text_plain( + req.content, + )), + ) + .await + { + Ok(msg) => msg, + Err(e) => { + log::error!( + "Failed to created edit message event {}: {e}", + event_path.event_id + ); + return Ok(HttpResponse::InternalServerError() + .json(format!("Failed to create edit message event! {e}"))); + } + }; + + Ok(match room.send(edit_event).await { + Ok(_) => HttpResponse::Accepted().finish(), + Err(e) => { + log::error!("Failed to edit event message {}: {e}", event_path.event_id); + HttpResponse::InternalServerError().json(format!("Failed to edit event! {e}")) + } + }) +} + pub async fn redact_event( client: MatrixClientExtractor, path: web::Path, diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 09ae619..6814d0f 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -173,6 +173,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/{room_id}/send_text_message", web::post().to(matrix_event_controller::send_text_message), ) + .route( + "/api/matrix/room/{room_id}/event/{event_id}/set_text_content", + web::post().to(matrix_event_controller::set_text_content), + ) .route( "/api/matrix/room/{room_id}/event/{event_id}", web::delete().to(matrix_event_controller::redact_event), From 0a37688116276b20fd12e9630132d7283f1d8f9c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 13:40:14 +0100 Subject: [PATCH 067/124] Can react to event --- .../matrix/matrix_event_controller.rs | 24 +++++++++++++++++++ matrixgw_backend/src/main.rs | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index 7befb98..f6ab6e7 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -7,6 +7,8 @@ use matrix_sdk::Room; use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind}; use matrix_sdk::room::MessagesOptions; use matrix_sdk::room::edit::EditedContent; +use matrix_sdk::ruma::events::reaction::ReactionEventContent; +use matrix_sdk::ruma::events::relation::Annotation; use matrix_sdk::ruma::events::room::message::{ RoomMessageEventContent, RoomMessageEventContentWithoutRelation, }; @@ -160,6 +162,28 @@ pub async fn set_text_content( }) } +#[derive(Deserialize)] +struct EventReactionBody { + key: String, +} + +pub async fn react_to_event( + client: MatrixClientExtractor, + path: web::Path, + event_path: web::Path, +) -> HttpResult { + let body = client.auth.decode_json_body::()?; + + let Some(room) = client.client.client.get_room(&path.room_id) else { + return Ok(HttpResponse::NotFound().json("Room not found!")); + }; + + let annotation = Annotation::new(event_path.event_id.to_owned(), body.key.to_owned()); + room.send(ReactionEventContent::from(annotation)).await?; + + Ok(HttpResponse::Accepted().finish()) +} + pub async fn redact_event( client: MatrixClientExtractor, path: web::Path, diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 6814d0f..f79d018 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -177,6 +177,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/{room_id}/event/{event_id}/set_text_content", web::post().to(matrix_event_controller::set_text_content), ) + .route( + "/api/matrix/room/{room_id}/event/{event_id}/react", + web::post().to(matrix_event_controller::react_to_event), + ) .route( "/api/matrix/room/{room_id}/event/{event_id}", web::delete().to(matrix_event_controller::redact_event), From 820b095be0b6493b97e504e2714c6c6732029e3f Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 16:05:01 +0100 Subject: [PATCH 068/124] Display the list of spaces --- .../src/api/matrix/MatrixApiEvent.ts | 5 ++ .../src/api/matrix/MatrixApiMedia.ts | 12 +++++ .../src/api/matrix/MatrixApiProfile.ts | 26 +++++++++ .../src/api/matrix/MatrixApiRoom.ts | 27 ++++++++++ matrixgw_frontend/src/routes/HomeRoute.tsx | 15 +----- .../dashboard/BaseAuthenticatedPage.tsx | 11 ++-- .../src/widgets/dashboard/DashboardHeader.tsx | 2 +- .../widgets/dashboard/DashboardSidebar.tsx | 2 +- .../widgets/messages/MainMessagesWidget.tsx | 54 +++++++++++++++++++ .../src/widgets/messages/RoomIcon.tsx | 32 +++++++++++ .../src/widgets/messages/SpaceSelector.tsx | 53 ++++++++++++++++++ 11 files changed, 218 insertions(+), 21 deletions(-) create mode 100644 matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts create mode 100644 matrixgw_frontend/src/api/matrix/MatrixApiMedia.ts create mode 100644 matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts create mode 100644 matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts create mode 100644 matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx create mode 100644 matrixgw_frontend/src/widgets/messages/RoomIcon.tsx create mode 100644 matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts new file mode 100644 index 0000000..ee0dfd6 --- /dev/null +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -0,0 +1,5 @@ +export interface MatrixEvent { + id: string; + time: number; + sender: string; +} diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiMedia.ts b/matrixgw_frontend/src/api/matrix/MatrixApiMedia.ts new file mode 100644 index 0000000..7d455da --- /dev/null +++ b/matrixgw_frontend/src/api/matrix/MatrixApiMedia.ts @@ -0,0 +1,12 @@ +import { APIClient } from "../ApiClient"; + +export class MatrixApiMedia { + /** + * Get media URL + */ + static MediaURL(url: string, thumbnail: boolean): string { + return `${APIClient.ActualBackendURL()}/matrix/media/${encodeURIComponent( + url + )}?thumbnail=${thumbnail}`; + } +} diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts b/matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts new file mode 100644 index 0000000..ccb39db --- /dev/null +++ b/matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts @@ -0,0 +1,26 @@ +import { APIClient } from "../ApiClient"; + +export interface UserProfile { + user_id: string; + display_name?: string; + avatar?: string; +} + +export type UsersMap = Map; + +export class MatrixApiProfile { + /** + * Get multiple profiles information + */ + static async GetMultiple(ids: string[]): Promise { + const list: UserProfile[] = ( + await APIClient.exec({ + method: "POST", + uri: "/matrix/profile/get_multiple", + jsonData: ids, + }) + ).data; + + return new Map(list.map((e) => [e.user_id, e])); + } +} diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts new file mode 100644 index 0000000..39fa989 --- /dev/null +++ b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts @@ -0,0 +1,27 @@ +import { APIClient } from "../ApiClient"; +import type { MatrixEvent } from "./MatrixApiEvent"; + +export interface Room { + id: string; + name?: string; + members: string[]; + avatar?: string; + is_space?: boolean; + parents: string[]; + number_unread_messages: number; + latest_event?: MatrixEvent; +} + +export class MatrixApiRoom { + /** + * Get the list of joined rooms + */ + static async ListJoined(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/matrix/room/joined", + }) + ).data; + } +} diff --git a/matrixgw_frontend/src/routes/HomeRoute.tsx b/matrixgw_frontend/src/routes/HomeRoute.tsx index 829833c..f046f41 100644 --- a/matrixgw_frontend/src/routes/HomeRoute.tsx +++ b/matrixgw_frontend/src/routes/HomeRoute.tsx @@ -1,6 +1,5 @@ -import { MatrixSyncApi } from "../api/MatrixSyncApi"; -import { AsyncWidget } from "../widgets/AsyncWidget"; import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; +import { MainMessageWidget } from "../widgets/messages/MainMessagesWidget"; import { NotLinkedAccountMessage } from "../widgets/NotLinkedAccountMessage"; export function HomeRoute(): React.ReactElement { @@ -8,15 +7,5 @@ export function HomeRoute(): React.ReactElement { if (!user.info.matrix_account_connected) return ; - return ( -

    - Todo home route{" "} - <>sync started} - /> -

    - ); + return ; } diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index 338fda7..0cbee66 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -1,4 +1,4 @@ -import { Button } from "@mui/material"; +import { AppBar, Button } from "@mui/material"; import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import Toolbar from "@mui/material/Toolbar"; @@ -105,6 +105,10 @@ export default function BaseAuthenticatedPage(): React.ReactElement { signOut, }} > + - - + ( - (); + const [users, setUsers] = React.useState(); + + const load = async () => { + await MatrixSyncApi.Start(); + + const rooms = await MatrixApiRoom.ListJoined(); + setRooms(rooms); + + // Get the list of users in rooms + const users = rooms.reduce((prev, r) => { + r.members.forEach((m) => prev.add(m)); + return prev; + }, new Set()); + + setUsers(await MatrixApiProfile.GetMultiple([...users])); + }; + + return ( + <_MainMessageWidget rooms={rooms!} users={users!} />} + /> + ); +} + +function _MainMessageWidget(p: { + rooms: Room[]; + users: UsersMap; +}): React.ReactElement { + const [space, setSpace] = React.useState(); + return ( +
    + + + todo +
    + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx b/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx new file mode 100644 index 0000000..6c49338 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx @@ -0,0 +1,32 @@ +import { Icon } from "@mui/material"; +import { MatrixApiMedia } from "../../api/matrix/MatrixApiMedia"; +import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; +import type { Room } from "../../api/matrix/MatrixApiRoom"; +import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; +import GroupIcon from "@mui/icons-material/Group"; + +export function RoomIcon(p: { + room: Room; + users: UsersMap; +}): React.ReactElement { + const user = useUserInfo(); + + let url = p.room.avatar; + + if (!url && p.room.members.length <= 1) url = p.room.members[0]; + + if (!url && p.room.members.length < 2) + url = + p.room.members[0] == user.info.matrix_user_id + ? p.room.members[1] + : p.room.members[0]; + + if (!url) return ; + else + return ( + + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx b/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx new file mode 100644 index 0000000..8720325 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx @@ -0,0 +1,53 @@ +import HomeIcon from "@mui/icons-material/Home"; +import { Button } from "@mui/material"; +import React from "react"; +import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; +import type { Room } from "../../api/matrix/MatrixApiRoom"; +import { RoomIcon } from "./RoomIcon"; + +export function SpaceSelector(p: { + rooms: Room[]; + users: UsersMap; + selectedSpace?: string; + onChange: (space?: string) => void; +}): React.ReactElement { + const spaces = React.useMemo( + () => p.rooms.filter((r) => r.is_space), + [p.rooms] + ); + + return ( +
    + } + onClick={() => p.onChange()} + selected={p.selectedSpace === undefined} + /> + + {spaces.map((s) => ( + } + onClick={() => p.onChange(s.id)} + selected={p.selectedSpace === s.id} + /> + ))} +
    + ); +} + +function SpaceButton(p: { + selected?: boolean; + icon: React.ReactElement; + onClick: () => void; +}): React.ReactElement { + return ( + + ); +} From cce9b3de5da4011c7a03829dc64981f8285ce1ed Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 16:36:36 +0100 Subject: [PATCH 069/124] Hide menu by default on desktop --- .../dashboard/BaseAuthenticatedPage.tsx | 3 +- .../widgets/dashboard/DashboardSidebar.tsx | 44 ++++++++++--------- .../dashboard/DashboardSidebarContext.tsx | 1 - .../dashboard/DashboardSidebarPageItem.tsx | 8 ++-- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index 0cbee66..886b8dc 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -1,7 +1,6 @@ -import { AppBar, Button } from "@mui/material"; +import { Button } from "@mui/material"; import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; -import Toolbar from "@mui/material/Toolbar"; import useMediaQuery from "@mui/material/useMediaQuery"; import * as React from "react"; import { Outlet, useNavigate } from "react-router"; diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx index 7e2ac26..562c496 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx @@ -3,7 +3,6 @@ import Icon from "@mdi/react"; import Box from "@mui/material/Box"; import Drawer from "@mui/material/Drawer"; import List from "@mui/material/List"; -import Toolbar from "@mui/material/Toolbar"; import { useTheme } from "@mui/material/styles"; import type {} from "@mui/material/themeCssVarsAugmentation"; import useMediaQuery from "@mui/material/useMediaQuery"; @@ -28,7 +27,6 @@ export interface DashboardSidebarProps { export default function DashboardSidebar({ expanded = true, setExpanded, - disableCollapsibleSidebar = false, container, }: DashboardSidebarProps) { const theme = useTheme(); @@ -53,8 +51,6 @@ export default function DashboardSidebar({ return () => {}; }, [expanded, theme.transitions.duration.enteringScreen]); - const mini = !disableCollapsibleSidebar && !expanded; - const handleSetSidebarExpanded = React.useCallback( (newExpanded: boolean) => () => { setExpanded(newExpanded); @@ -66,10 +62,9 @@ export default function DashboardSidebar({ if (!isOverSmViewport) { setExpanded(false); } - }, [mini, setExpanded, isOverSmViewport]); + }, [expanded, setExpanded, isOverSmViewport]); - const hasDrawerTransitions = - isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport); + const hasDrawerTransitions = isOverSmViewport && isOverMdViewport; const getDrawerContent = React.useCallback( (viewport: "phone" | "tablet" | "desktop") => ( @@ -83,9 +78,9 @@ export default function DashboardSidebar({ flexDirection: "column", justifyContent: "space-between", overflow: "auto", - scrollbarGutter: mini ? "stable" : "auto", + scrollbarGutter: !expanded ? "stable" : "auto", overflowX: "hidden", - pt: !mini ? 0 : 2, + pt: expanded ? 0 : 2, paddingTop: 0, ...(hasDrawerTransitions ? getDrawerSxTransitionMixin(isFullyExpanded, "padding") @@ -95,9 +90,9 @@ export default function DashboardSidebar({ } href="/" + mini={viewport === "desktop"} /> } href="/matrix_link" + mini={viewport === "desktop"} /> } href="/tokens" + mini={viewport === "desktop"} /> } href="/wsdebug" + mini={viewport === "desktop"} />
    ), [ - mini, + expanded, hasDrawerTransitions, isFullyExpanded, user.info.matrix_account_connected, @@ -136,8 +135,14 @@ export default function DashboardSidebar({ ); const getDrawerSharedSx = React.useCallback( - (isTemporary: boolean) => { - const drawerWidth = mini ? MINI_DRAWER_WIDTH : DRAWER_WIDTH; + (isTemporary: boolean, desktop?: boolean) => { + const drawerWidth = desktop + ? expanded + ? MINI_DRAWER_WIDTH + : 0 + : !expanded + ? MINI_DRAWER_WIDTH + : DRAWER_WIDTH; return { displayPrint: "none", @@ -154,17 +159,16 @@ export default function DashboardSidebar({ }, }; }, - [expanded, mini] + [expanded, !expanded] ); const sidebarContextValue = React.useMemo(() => { return { onPageItemClick: handlePageItemClick, - mini, fullyExpanded: isFullyExpanded, hasDrawerTransitions, }; - }, [handlePageItemClick, mini, isFullyExpanded, hasDrawerTransitions]); + }, [handlePageItemClick, !expanded, isFullyExpanded, hasDrawerTransitions]); return ( @@ -179,7 +183,7 @@ export default function DashboardSidebar({ sx={{ display: { xs: "block", - sm: disableCollapsibleSidebar ? "block" : "none", + sm: "none", md: "none", }, ...getDrawerSharedSx(true), @@ -192,7 +196,7 @@ export default function DashboardSidebar({ sx={{ display: { xs: "none", - sm: disableCollapsibleSidebar ? "none" : "block", + sm: "block", md: "none", }, ...getDrawerSharedSx(false), @@ -204,7 +208,7 @@ export default function DashboardSidebar({ variant="permanent" sx={{ display: { xs: "none", md: "block" }, - ...getDrawerSharedSx(false), + ...getDrawerSharedSx(false, true), }} > {getDrawerContent("desktop")} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx index 751b63b..ef1e8ee 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx @@ -2,7 +2,6 @@ import * as React from "react"; const DashboardSidebarContext = React.createContext<{ onPageItemClick: () => void; - mini: boolean; fullyExpanded: boolean; hasDrawerTransitions: boolean; } | null>(null); diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx index 5d32561..ef83b58 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx @@ -17,6 +17,7 @@ export interface DashboardSidebarPageItemProps { href: string; action?: React.ReactNode; disabled?: boolean; + mini?: boolean; } export default function DashboardSidebarPageItem({ @@ -25,6 +26,7 @@ export default function DashboardSidebarPageItem({ href, action, disabled = false, + mini = false, }: DashboardSidebarPageItemProps) { const { pathname } = useLocation(); @@ -32,11 +34,7 @@ export default function DashboardSidebarPageItem({ if (!sidebarContext) { throw new Error("Sidebar context was used without a provider."); } - const { - onPageItemClick, - mini = false, - fullyExpanded = true, - } = sidebarContext; + const { onPageItemClick, fullyExpanded = true } = sidebarContext; const hasExternalHref = href ? href.startsWith("http://") || href.startsWith("https://") From 1f4e374e663d9f3369b48a5bc58ead9b830753b2 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 17:50:31 +0100 Subject: [PATCH 070/124] Display rooms list --- .../src/api/matrix/MatrixApiRoom.ts | 28 ++++++++ matrixgw_frontend/src/index.css | 9 +++ .../dashboard/BaseAuthenticatedPage.tsx | 2 - .../src/widgets/dashboard/DashboardHeader.tsx | 6 +- .../widgets/messages/MainMessagesWidget.tsx | 18 +++++ .../src/widgets/messages/RoomIcon.tsx | 37 +++++----- .../src/widgets/messages/RoomSelector.tsx | 69 +++++++++++++++++++ 7 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 matrixgw_frontend/src/widgets/messages/RoomSelector.tsx diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts index 39fa989..fecbd21 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts @@ -1,5 +1,7 @@ import { APIClient } from "../ApiClient"; +import type { UserInfo } from "../AuthApi"; import type { MatrixEvent } from "./MatrixApiEvent"; +import type { UsersMap } from "./MatrixApiProfile"; export interface Room { id: string; @@ -12,6 +14,32 @@ export interface Room { latest_event?: MatrixEvent; } +/** + * Find main member of room + */ +export function mainRoomMember(user: UserInfo, r: Room): string | undefined { + if (r.members.length <= 1) return r.members[0]; + + if (r.members.length < 2) + return r.members[0] == user.matrix_user_id ? r.members[1] : r.members[0]; + + return undefined; +} + +/** + * Find room name + */ +export function roomName(user: UserInfo, r: Room, users: UsersMap): string { + if (r.name) return r.name; + + const name = r.members + .filter((m) => m !== user.matrix_user_id) + .map((m) => users.get(m)?.display_name ?? m) + .join(","); + + return name === "" ? "Empty room" : name; +} + export class MatrixApiRoom { /** * Get the list of joined rooms diff --git a/matrixgw_frontend/src/index.css b/matrixgw_frontend/src/index.css index b303f80..ba4562c 100644 --- a/matrixgw_frontend/src/index.css +++ b/matrixgw_frontend/src/index.css @@ -7,3 +7,12 @@ body, #root { height: 100%; } + +#root { + display: flex; + flex-direction: column; +} + +#root > div { + flex: 1; +} diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx index 886b8dc..81f706e 100644 --- a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -114,8 +114,6 @@ export default function BaseAuthenticatedPage(): React.ReactElement { position: "relative", display: "flex", overflow: "hidden", - height: "100%", - width: "100%", }} > + (); @@ -44,10 +45,27 @@ function _MainMessageWidget(p: { users: UsersMap; }): React.ReactElement { const [space, setSpace] = React.useState(); + const [room, setRoom] = React.useState(); + + const spaceRooms = React.useMemo(() => { + return p.rooms + .filter((r) => !r.is_space && (!space || r.parents.includes(space))) + .sort( + (a, b) => (b.latest_event?.time ?? 0) - (a.latest_event?.time ?? 0) + ); + }, [space, p.rooms]); + return (
    + + todo
    ); diff --git a/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx b/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx index 6c49338..8efafac 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx @@ -1,9 +1,12 @@ -import { Icon } from "@mui/material"; +import { Avatar } from "@mui/material"; import { MatrixApiMedia } from "../../api/matrix/MatrixApiMedia"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; -import type { Room } from "../../api/matrix/MatrixApiRoom"; +import { + mainRoomMember, + roomName, + type Room, +} from "../../api/matrix/MatrixApiRoom"; import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; -import GroupIcon from "@mui/icons-material/Group"; export function RoomIcon(p: { room: Room; @@ -13,20 +16,18 @@ export function RoomIcon(p: { let url = p.room.avatar; - if (!url && p.room.members.length <= 1) url = p.room.members[0]; + if (!url) { + const member = mainRoomMember(user.info, p.room); + if (member) url = p.users.get(member)?.avatar; + } + const name = roomName(user.info, p.room, p.users); - if (!url && p.room.members.length < 2) - url = - p.room.members[0] == user.info.matrix_user_id - ? p.room.members[1] - : p.room.members[0]; - - if (!url) return ; - else - return ( - - ); + return ( + + {name.slice(0, 1)} + + ); } diff --git a/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx b/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx new file mode 100644 index 0000000..cbdba06 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx @@ -0,0 +1,69 @@ +import { + Chip, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, +} from "@mui/material"; +import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; +import { roomName, type Room } from "../../api/matrix/MatrixApiRoom"; +import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; +import { RoomIcon } from "./RoomIcon"; + +const ROOM_SELECTOR_WIDTH = "300px"; + +export function RoomSelector(p: { + users: UsersMap; + rooms: Room[]; + currRoom?: Room; + onChange: (r: Room) => void; +}): React.ReactElement { + const user = useUserInfo(); + + if (p.rooms.length === 0) + return ( +
    + No room to display. +
    + ); + + return ( + + {p.rooms.map((r) => ( + + ) + } + disablePadding + > + p.onChange(r)} + dense + selected={p.currRoom?.id === r.id} + > + + + + + + + ))} + + ); +} From 4be661d9996fc36ccdbd8bb45db450555c60af35 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 17:55:26 +0100 Subject: [PATCH 071/124] Fix appearance of unread conversations --- .../controllers/matrix/matrix_room_controller.rs | 2 +- .../src/widgets/messages/RoomSelector.tsx | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index 0751f92..1a64948 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -51,7 +51,7 @@ impl APIRoomInfo { avatar: r.avatar_url(), is_space: r.is_space(), parents: parent_spaces, - number_unread_messages: r.num_unread_messages(), + number_unread_messages: r.unread_notification_counts().notification_count, latest_event: get_events(r, 1, None).await?.messages.into_iter().next(), }) } diff --git a/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx b/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx index cbdba06..c908696 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx @@ -60,7 +60,18 @@ export function RoomSelector(p: { - + 0 ? "bold" : undefined, + }} + > + {roomName(user.info, r, p.users)} + + } + /> ))} From a7bfd713c3e1ed6881b04bc5383549fead6a6b3f Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 24 Nov 2025 17:59:12 +0100 Subject: [PATCH 072/124] Ready to implement room widget --- .../widgets/messages/MainMessagesWidget.tsx | 19 ++++++++++++++++--- .../src/widgets/messages/RoomWidget.tsx | 9 +++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 matrixgw_frontend/src/widgets/messages/RoomWidget.tsx diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index b96d348..d8ec52a 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -1,3 +1,4 @@ +import { Divider } from "@mui/material"; import React from "react"; import { MatrixApiProfile, @@ -6,9 +7,9 @@ import { import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom"; import { MatrixSyncApi } from "../../api/MatrixSyncApi"; import { AsyncWidget } from "../AsyncWidget"; -import { SpaceSelector } from "./SpaceSelector"; -import { Divider } from "@mui/material"; import { RoomSelector } from "./RoomSelector"; +import { RoomWidget } from "./RoomWidget"; +import { SpaceSelector } from "./SpaceSelector"; export function MainMessageWidget(): React.ReactElement { const [rooms, setRooms] = React.useState(); @@ -66,7 +67,19 @@ function _MainMessageWidget(p: { onChange={setRoom} /> - todo + {room === undefined && ( +
    + No room selected. +
    + )} + {room && }
    ); } diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx new file mode 100644 index 0000000..be400ad --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -0,0 +1,9 @@ +import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; +import type { Room } from "../../api/matrix/MatrixApiRoom"; + +export function RoomWidget(p: { + room: Room; + users: UsersMap; +}): React.ReactElement { + return <>room; +} From 5eab7c3e4f15054722ba74bd0a592862cbb5320a Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 25 Nov 2025 09:48:49 +0100 Subject: [PATCH 073/124] Process events list client side --- .../matrix/matrix_event_controller.rs | 4 +- .../matrix/matrix_room_controller.rs | 2 +- .../src/api/matrix/MatrixApiEvent.ts | 65 +++++++++++ .../src/utils/RoomEventsManager.ts | 101 ++++++++++++++++++ .../src/widgets/messages/RoomWidget.tsx | 23 +++- 5 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 matrixgw_frontend/src/utils/RoomEventsManager.ts diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index f6ab6e7..9064ccb 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -48,7 +48,7 @@ impl APIEvent { pub struct APIEventsList { pub start: String, pub end: Option, - pub messages: Vec, + pub events: Vec, } /// Get messages for a given room @@ -65,7 +65,7 @@ pub(super) async fn get_events( Ok(APIEventsList { start: messages.start, end: messages.end, - messages: stream::iter(messages.chunk) + events: stream::iter(messages.chunk) .then(async |msg| APIEvent::from_evt(msg, room.room_id()).await) .collect::>() .await diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index 1a64948..20461e9 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -52,7 +52,7 @@ impl APIRoomInfo { is_space: r.is_space(), parents: parent_spaces, number_unread_messages: r.unread_notification_counts().notification_count, - latest_event: get_events(r, 1, None).await?.messages.into_iter().next(), + latest_event: get_events(r, 1, None).await?.events.into_iter().next(), }) } } diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index ee0dfd6..01c2d9a 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -1,5 +1,70 @@ +import { APIClient } from "../ApiClient"; +import type { Room } from "./MatrixApiRoom"; + +export interface MatrixRoomMessage { + type: "m.room.message"; + content: { + body: string; + msgtype: "m.text" | "m.image" | string; + "m.relates_to"?: { + event_id: string; + rel_type: "m.replace" | string; + }; + file?: { + url: string; + }; + }; +} + +export interface MatrixReaction { + type: "m.reaction"; + content: { + "m.relates_to": { + event_id: string; + key: string; + }; + }; +} + +export interface MatrixRoomRedaction { + type: "m.room.redaction"; + redacts: string; +} + +export type MatrixEventData = + | MatrixRoomMessage + | MatrixReaction + | MatrixRoomRedaction + | { type: "other" }; + export interface MatrixEvent { id: string; time: number; sender: string; + data: MatrixEventData; +} + +export interface MatrixEventsList { + start: string; + end?: string; + events: MatrixEvent[]; +} + +export class MatrixApiEvent { + /** + * Get Matrix room events + */ + static async GetRoomEvents( + room: Room, + from?: string + ): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: + `/matrix/room/${encodeURIComponent(room.id)}/events` + + (from ? `?from=${from}` : ""), + }) + ).data; + } } diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts new file mode 100644 index 0000000..78dcedb --- /dev/null +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -0,0 +1,101 @@ +import type { + MatrixEvent, + MatrixEventsList, +} from "../api/matrix/MatrixApiEvent"; +import type { Room } from "../api/matrix/MatrixApiRoom"; + +export interface MessageReaction { + event_id: string; + account: string; + key: string; +} + +export interface Message { + event_id: string; + sent: number; + modified: boolean; + reactions: MessageReaction[]; + content: string; + image?: string; +} + +export class RoomEventsManager { + readonly room: Room; + private events: MatrixEvent[]; + messages: Message[]; + endToken?: string; + + constructor(room: Room, initialMessages: MatrixEventsList) { + this.room = room; + this.events = []; + this.messages = []; + + this.processNewEvents(initialMessages); + } + + /** + * Process events given by the API + */ + processNewEvents(evts: MatrixEventsList) { + this.endToken = evts.end; + this.events = [...this.events, ...evts.events]; + this.rebuildMessagesList(); + } + + private rebuildMessagesList() { + // Sorts events list to process oldest events first + this.events.sort((a, b) => a.time - b.time); + + // First, process redactions to skip redacted events + let redacted = new Set( + this.events + .map((e) => + e.data.type === "m.room.redaction" ? e.data.redacts : undefined + ) + .filter((e) => e !== undefined) + ); + + for (const evt of this.events) { + if (redacted.has(evt.id)) continue; + + const data = evt.data; + + // Message + if (data.type === "m.room.message") { + // Check if this message replaces another one + if (data.content["m.relates_to"]) { + const message = this.messages.find( + (m) => m.event_id === data.content["m.relates_to"]?.event_id + ); + if (!message) continue; + message.modified = true; + message.content = data.content.body; + continue; + } + + this.messages.push({ + event_id: evt.id, + modified: false, + reactions: [], + sent: evt.time, + image: data.content.file?.url, + content: data.content.body, + }); + } + + // Reaction + if (data.type === "m.reaction") { + const message = this.messages.find( + (m) => m.event_id === data.content["m.relates_to"].event_id + ); + + if (!message) continue; + message.reactions.push({ + account: evt.sender, + event_id: evt.id, + key: data.content["m.relates_to"].key, + }); + } + } + } +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index be400ad..de26ea7 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -1,9 +1,30 @@ +import React from "react"; +import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; +import { RoomEventsManager } from "../../utils/RoomEventsManager"; +import { AsyncWidget } from "../AsyncWidget"; export function RoomWidget(p: { room: Room; users: UsersMap; }): React.ReactElement { - return <>room; + const [roomMgr, setRoomMgr] = React.useState(); + + const load = async () => { + setRoomMgr(undefined); + const messages = await MatrixApiEvent.GetRoomEvents(p.room); + const mgr = new RoomEventsManager(p.room, messages); + setRoomMgr(mgr); + }; + + return ( + <>room} + /> + ); } From 2adbf146d0834965dd2062c73276c2c68e5019a4 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 25 Nov 2025 12:17:48 +0100 Subject: [PATCH 074/124] Start to display messages list --- .../src/utils/RoomEventsManager.ts | 6 +- .../src/widgets/messages/AccountIcon.tsx | 15 +++++ .../src/widgets/messages/RoomMessagesList.tsx | 61 +++++++++++++++++++ .../src/widgets/messages/RoomWidget.tsx | 8 ++- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 matrixgw_frontend/src/widgets/messages/AccountIcon.tsx create mode 100644 matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 78dcedb..4f74bb1 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -12,7 +12,8 @@ export interface MessageReaction { export interface Message { event_id: string; - sent: number; + account: string; + time_sent: number; modified: boolean; reactions: MessageReaction[]; content: string; @@ -75,9 +76,10 @@ export class RoomEventsManager { this.messages.push({ event_id: evt.id, + account: evt.sender, modified: false, reactions: [], - sent: evt.time, + time_sent: evt.time, image: data.content.file?.url, content: data.content.body, }); diff --git a/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx b/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx new file mode 100644 index 0000000..3c6e23a --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx @@ -0,0 +1,15 @@ +import { Avatar } from "@mui/material"; +import type { UserProfile } from "../../api/matrix/MatrixApiProfile"; +import { MatrixApiMedia } from "../../api/matrix/MatrixApiMedia"; + +export function AccountIcon(p: { user: UserProfile }): React.ReactElement { + return ( + + {p.user.display_name?.slice(0, 1)} + + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx new file mode 100644 index 0000000..1848fcd --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -0,0 +1,61 @@ +import dayjs from "dayjs"; +import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; +import type { Message, RoomEventsManager } from "../../utils/RoomEventsManager"; +import { AccountIcon } from "./AccountIcon"; +import { MatrixApiMedia } from "../../api/matrix/MatrixApiMedia"; + +export function RoomMessagesList(p: { + users: UsersMap; + mgr: RoomEventsManager; +}): React.ReactElement { + return ( +
    + {p.mgr.messages.map((m, idx) => ( + 0 && + p.mgr.messages[idx - 1].account === m.account && + m.time_sent - p.mgr.messages[idx - 1].time_sent < 60 * 3 * 1000 + } + /> + ))} +
    + ); +} + +function RoomMessage(p: { + users: UsersMap; + message: Message; + previousFromSamePerson: boolean; +}): React.ReactElement { + const user = p.users.get(p.message.account); + return ( + <> + {!p.previousFromSamePerson && user && ( +
    + +   + {user.display_name} +
    + )} + +
    + {dayjs.unix(p.message.time_sent / 1000).format("HH:mm")}{" "} + {p.message.image ? ( + + ) : ( + p.message.content + )} +
    + + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index de26ea7..a5aeaf0 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -4,6 +4,7 @@ import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { AsyncWidget } from "../AsyncWidget"; +import { RoomMessagesList } from "./RoomMessagesList"; export function RoomWidget(p: { room: Room; @@ -24,7 +25,12 @@ export function RoomWidget(p: { ready={!!roomMgr} load={load} errMsg="Failed to load room!" - build={() => <>room} + build={() => ( +
    + +
    Send message form
    +
    + )} /> ); } From b7378aa4dc45e65b879d48cf298269334aa61e24 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 25 Nov 2025 14:54:02 +0100 Subject: [PATCH 075/124] Can retrieve room media --- .../matrix/matrix_event_controller.rs | 70 ++++++++++++++++++- .../matrix/matrix_media_controller.rs | 53 +++++++++----- .../matrix/matrix_room_controller.rs | 2 +- matrixgw_backend/src/controllers/mod.rs | 6 ++ matrixgw_backend/src/main.rs | 6 +- .../src/api/matrix/MatrixApiEvent.ts | 13 ++++ .../src/utils/RoomEventsManager.ts | 3 + .../src/widgets/messages/RoomMessagesList.tsx | 27 +++++-- 8 files changed, 156 insertions(+), 24 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index 9064ccb..f5eab4c 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -1,17 +1,22 @@ use crate::controllers::HttpResult; +use crate::controllers::matrix::matrix_media_controller; +use crate::controllers::matrix::matrix_media_controller::MediaQuery; use crate::controllers::matrix::matrix_room_controller::RoomIdInPath; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; -use actix_web::{HttpResponse, web}; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; use futures_util::{StreamExt, stream}; use matrix_sdk::Room; use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind}; +use matrix_sdk::media::MediaEventContent; use matrix_sdk::room::MessagesOptions; use matrix_sdk::room::edit::EditedContent; use matrix_sdk::ruma::events::reaction::ReactionEventContent; use matrix_sdk::ruma::events::relation::Annotation; use matrix_sdk::ruma::events::room::message::{ - RoomMessageEventContent, RoomMessageEventContentWithoutRelation, + MessageType, RoomMessageEvent, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, }; +use matrix_sdk::ruma::events::{AnyMessageLikeEvent, AnyTimelineEvent}; use matrix_sdk::ruma::{MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedUserId, RoomId, UInt}; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; @@ -162,6 +167,67 @@ pub async fn set_text_content( }) } +pub async fn event_file( + req: HttpRequest, + client: MatrixClientExtractor, + path: web::Path, + event_path: web::Path, +) -> HttpResult { + let query = web::Query::::from_request(&req, &mut Payload::None).await?; + + let Some(room) = client.client.client.get_room(&path.room_id) else { + return Ok(HttpResponse::NotFound().json("Room not found!")); + }; + + let event = match room.load_or_fetch_event(&event_path.event_id, None).await { + Ok(event) => event, + Err(e) => { + log::error!("Failed to load event information! {e}"); + return Ok(HttpResponse::InternalServerError() + .json(format!("Failed to load event information! {e}"))); + } + }; + + let event = match event.kind { + TimelineEventKind::Decrypted(dec) => dec.event.deserialize()?, + TimelineEventKind::UnableToDecrypt { event, .. } + | TimelineEventKind::PlainText { event } => event + .deserialize()? + .into_full_event(room.room_id().to_owned()), + }; + + let AnyTimelineEvent::MessageLike(message) = event else { + return Ok(HttpResponse::BadRequest().json("Event is not message like!")); + }; + + let AnyMessageLikeEvent::RoomMessage(message) = message else { + return Ok(HttpResponse::BadRequest().json("Event is not a room message!")); + }; + + let RoomMessageEvent::Original(message) = message else { + return Ok(HttpResponse::BadRequest().json("Event has been redacted!")); + }; + + let (source, thumb_source) = match message.content.msgtype { + MessageType::Audio(c) => (c.source(), c.thumbnail_source()), + MessageType::File(c) => (c.source(), c.thumbnail_source()), + MessageType::Image(c) => (c.source(), c.thumbnail_source()), + MessageType::Location(c) => (c.source(), c.thumbnail_source()), + MessageType::Video(c) => (c.source(), c.thumbnail_source()), + _ => (None, None), + }; + + println!("{source:#?} {thumb_source:#?}"); + + let source = match (query.thumbnail, source, thumb_source) { + (false, Some(s), _) => s, + (true, _, Some(s)) => s, + _ => return Ok(HttpResponse::NotFound().json("Requested file not available!")), + }; + + matrix_media_controller::serve_media(req, source, false).await +} + #[derive(Deserialize)] struct EventReactionBody { key: String, diff --git a/matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs index 51c485f..659b910 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs @@ -4,19 +4,35 @@ use crate::utils::crypt_utils::sha512; use actix_web::dev::Payload; use actix_web::http::header; use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; +use matrix_sdk::crypto::{AttachmentDecryptor, MediaEncryptionInfo}; use matrix_sdk::media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings}; use matrix_sdk::ruma::events::room::MediaSource; use matrix_sdk::ruma::{OwnedMxcUri, UInt}; +use std::io::{Cursor, Read}; #[derive(serde::Deserialize)] -struct MediaQuery { +pub struct MediaMXCInPath { + mxc: OwnedMxcUri, +} + +/// Serve media resource handler +pub async fn serve_mxc_handler(req: HttpRequest, media: web::Path) -> HttpResult { + serve_mxc_file(req, media.into_inner().mxc).await +} + +#[derive(serde::Deserialize)] +pub struct MediaQuery { #[serde(default)] - thumbnail: bool, + pub thumbnail: bool, +} +pub async fn serve_mxc_file(req: HttpRequest, media: OwnedMxcUri) -> HttpResult { + let query = web::Query::::from_request(&req, &mut Payload::None).await?; + + serve_media(req, MediaSource::Plain(media), query.thumbnail).await } /// Serve a media file -pub async fn serve_media(req: HttpRequest, media: OwnedMxcUri) -> HttpResult { - let query = web::Query::::from_request(&req, &mut Payload::None).await?; +pub async fn serve_media(req: HttpRequest, source: MediaSource, thumbnail: bool) -> HttpResult { let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?; let media = client @@ -25,8 +41,8 @@ pub async fn serve_media(req: HttpRequest, media: OwnedMxcUri) -> HttpResult { .media() .get_media_content( &MediaRequestParameters { - source: MediaSource::Plain(media), - format: match query.thumbnail { + source: source.clone(), + format: match thumbnail { true => MediaFormat::Thumbnail(MediaThumbnailSettings::new( UInt::new(100).unwrap(), UInt::new(100).unwrap(), @@ -38,6 +54,21 @@ pub async fn serve_media(req: HttpRequest, media: OwnedMxcUri) -> HttpResult { ) .await?; + // Decrypt file if needed + let media = if let MediaSource::Encrypted(file) = source { + let mut cursor = Cursor::new(media); + let mut decryptor = + AttachmentDecryptor::new(&mut cursor, MediaEncryptionInfo::from(*file))?; + + let mut decrypted_data = Vec::new(); + + decryptor.read_to_end(&mut decrypted_data)?; + + decrypted_data + } else { + media + }; + let digest = sha512(&media); let mime_type = infer::get(&media).map(|x| x.mime_type()); @@ -55,13 +86,3 @@ pub async fn serve_media(req: HttpRequest, media: OwnedMxcUri) -> HttpResult { .insert_header(("cache-control", "max-age=360000")) .body(media)) } - -#[derive(serde::Deserialize)] -pub struct MediaMXCInPath { - mxc: OwnedMxcUri, -} - -/// Save media resource handler -pub async fn serve_media_res(req: HttpRequest, media: web::Path) -> HttpResult { - serve_media(req, media.into_inner().mxc).await -} diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index 20461e9..1886169 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -111,5 +111,5 @@ pub async fn room_avatar( return Ok(HttpResponse::NotFound().json("Room has no avatar")); }; - matrix_media_controller::serve_media(req, uri).await + matrix_media_controller::serve_mxc_file(req, uri).await } diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index cab029c..75ceeed 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -24,6 +24,12 @@ pub enum HttpFailure { ActixError(#[from] actix_web::Error), #[error("Matrix error: {0}")] MatrixError(#[from] matrix_sdk::Error), + #[error("Matrix decryptor error: {0}")] + MatrixDecryptorError(#[from] matrix_sdk::encryption::DecryptorError), + #[error("Serde JSON error: {0}")] + SerdeJSON(#[from] serde_json::Error), + #[error("Standard library error: {0}")] + StdLibError(#[from] std::io::Error), } impl ResponseError for HttpFailure { diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index f79d018..7623f65 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -177,6 +177,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/{room_id}/event/{event_id}/set_text_content", web::post().to(matrix_event_controller::set_text_content), ) + .route( + "/api/matrix/room/{room_id}/event/{event_id}/file", + web::get().to(matrix_event_controller::event_file), + ) .route( "/api/matrix/room/{room_id}/event/{event_id}/react", web::post().to(matrix_event_controller::react_to_event), @@ -188,7 +192,7 @@ async fn main() -> std::io::Result<()> { // Matrix media controller .route( "/api/matrix/media/{mxc}", - web::get().to(matrix_media_controller::serve_media_res), + web::get().to(matrix_media_controller::serve_mxc_handler), ) }) .workers(4) diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index 01c2d9a..f24dd1b 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -67,4 +67,17 @@ export class MatrixApiEvent { }) ).data; } + + /** + * Get Matrix event file URL + */ + static GetEventFileURL( + room: Room, + event_id: string, + thumbnail: boolean + ): string { + return `${APIClient.ActualBackendURL()}/matrix/room/${ + room.id + }/event/${event_id}/file?thumbnail=${thumbnail}`; + } } diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 4f74bb1..5a8f72d 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -1,3 +1,4 @@ +import dayjs from "dayjs"; import type { MatrixEvent, MatrixEventsList, @@ -14,6 +15,7 @@ export interface Message { event_id: string; account: string; time_sent: number; + time_sent_dayjs: dayjs.Dayjs; modified: boolean; reactions: MessageReaction[]; content: string; @@ -80,6 +82,7 @@ export class RoomEventsManager { modified: false, reactions: [], time_sent: evt.time, + time_sent_dayjs: dayjs.unix(evt.time / 1000), image: data.content.file?.url, content: data.content.body, }); diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 1848fcd..1455a43 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -1,10 +1,11 @@ -import dayjs from "dayjs"; +import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; +import type { Room } from "../../api/matrix/MatrixApiRoom"; import type { Message, RoomEventsManager } from "../../utils/RoomEventsManager"; import { AccountIcon } from "./AccountIcon"; -import { MatrixApiMedia } from "../../api/matrix/MatrixApiMedia"; export function RoomMessagesList(p: { + room: Room; users: UsersMap; mgr: RoomEventsManager; }): React.ReactElement { @@ -20,6 +21,11 @@ export function RoomMessagesList(p: { p.mgr.messages[idx - 1].account === m.account && m.time_sent - p.mgr.messages[idx - 1].time_sent < 60 * 3 * 1000 } + firstMessageOfDay={ + idx === 0 || + m.time_sent_dayjs.startOf("day").unix() != + p.mgr.messages[idx - 1].time_sent_dayjs.startOf("day").unix() + } /> ))}
    @@ -27,13 +33,20 @@ export function RoomMessagesList(p: { } function RoomMessage(p: { + room: Room; users: UsersMap; message: Message; previousFromSamePerson: boolean; + firstMessageOfDay: boolean; }): React.ReactElement { const user = p.users.get(p.message.account); return ( <> + {p.firstMessageOfDay && ( +
    + {p.message.time_sent_dayjs.format("DD/MM/YYYY")} +
    + )} {!p.previousFromSamePerson && user && (
    - {dayjs.unix(p.message.time_sent / 1000).format("HH:mm")}{" "} + {p.message.time_sent_dayjs.format("HH:mm")}{" "} {p.message.image ? ( - + ) : ( p.message.content )} From bda47a277084d801a937922a9c0d3c1e811126fc Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 27 Nov 2025 18:55:30 +0100 Subject: [PATCH 076/124] Improve messages appearance --- .../src/widgets/messages/RoomMessagesList.tsx | 67 +++++++++++++++++-- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 1455a43..bd024f9 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -1,16 +1,34 @@ +import { Dialog, Typography } from "@mui/material"; import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; import type { Message, RoomEventsManager } from "../../utils/RoomEventsManager"; import { AccountIcon } from "./AccountIcon"; +import React from "react"; export function RoomMessagesList(p: { room: Room; users: UsersMap; mgr: RoomEventsManager; }): React.ReactElement { + const messagesEndRef = React.createRef(); + + // Automatically scroll to bottom when number of messages change + React.useEffect(() => { + if (messagesEndRef) + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [p.mgr.messages.length]); + return ( -
    +
    {p.mgr.messages.map((m, idx) => ( ))} + +
    ); } @@ -39,42 +59,75 @@ function RoomMessage(p: { previousFromSamePerson: boolean; firstMessageOfDay: boolean; }): React.ReactElement { + const [showImageFullScreen, setShowImageFullScreen] = React.useState(false); + + const closeImageFullScreen = () => setShowImageFullScreen(false); + const user = p.users.get(p.message.account); + return ( <> {p.firstMessageOfDay && ( -
    + {p.message.time_sent_dayjs.format("DD/MM/YYYY")} -
    + )} - {!p.previousFromSamePerson && user && ( + {(!p.previousFromSamePerson || p.firstMessageOfDay) && user && (
    -   +     {user.display_name}
    )} -
    - {p.message.time_sent_dayjs.format("HH:mm")}{" "} +
    + +   {p.message.time_sent_dayjs.format("HH:mm")} + {" "} +     {p.message.image ? ( setShowImageFullScreen(true)} src={MatrixApiEvent.GetEventFileURL( p.room, p.message.event_id, true )} + style={{ + maxWidth: "200px", + }} /> ) : ( p.message.content )}
    + + + + ); } From 9f83a6fb6692606a2981a2d3d3fba36429d09bf4 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Thu, 27 Nov 2025 19:13:16 +0100 Subject: [PATCH 077/124] Can send text messages in conversations --- .../src/api/matrix/MatrixApiEvent.ts | 11 ++++ .../src/widgets/messages/RoomWidget.tsx | 3 +- .../src/widgets/messages/SendMessageForm.tsx | 63 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 matrixgw_frontend/src/widgets/messages/SendMessageForm.tsx diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index f24dd1b..cd97463 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -80,4 +80,15 @@ export class MatrixApiEvent { room.id }/event/${event_id}/file?thumbnail=${thumbnail}`; } + + /** + * Send text message + */ + static async SendTextMessage(room: Room, content: string): Promise { + await APIClient.exec({ + method: "POST", + uri: `/matrix/room/${room.id}/send_text_message`, + jsonData: { content }, + }); + } } diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index a5aeaf0..e990666 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -5,6 +5,7 @@ import type { Room } from "../../api/matrix/MatrixApiRoom"; import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { AsyncWidget } from "../AsyncWidget"; import { RoomMessagesList } from "./RoomMessagesList"; +import { SendMessageForm } from "./SendMessageForm"; export function RoomWidget(p: { room: Room; @@ -28,7 +29,7 @@ export function RoomWidget(p: { build={() => (
    -
    Send message form
    +
    )} /> diff --git a/matrixgw_frontend/src/widgets/messages/SendMessageForm.tsx b/matrixgw_frontend/src/widgets/messages/SendMessageForm.tsx new file mode 100644 index 0000000..41b6ae0 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/SendMessageForm.tsx @@ -0,0 +1,63 @@ +import SendIcon from "@mui/icons-material/Send"; +import { IconButton, TextField } from "@mui/material"; +import React, { type FormEvent } from "react"; +import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; +import type { Room } from "../../api/matrix/MatrixApiRoom"; +import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider"; +import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider"; + +export function SendMessageForm(p: { room: Room }): React.ReactElement { + const loadingMessage = useLoadingMessage(); + const alert = useAlert(); + + const [text, setText] = React.useState(""); + + const handleTextSubmit = async (e: FormEvent) => { + e.preventDefault(); + + if (text === "") return; + + loadingMessage.show("Sending message..."); + + try { + await MatrixApiEvent.SendTextMessage(p.room, text); + + setText(""); + } catch (e) { + console.error(`Failed to send message! ${e}`); + alert(`Failed to send message! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + + return ( +
    +
    + setText(e.target.value)} + /> + + + +
    +
    + ); +} From 94ce9c3c951876c2c62a5b8165a5a89f49a79db3 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 09:55:21 +0100 Subject: [PATCH 078/124] Add buttons bar --- .../widgets/dashboard/DashboardSidebar.tsx | 17 +------- .../src/widgets/messages/RoomMessagesList.tsx | 40 +++++++++++++++++-- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx index 562c496..c733355 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx @@ -67,7 +67,7 @@ export default function DashboardSidebar({ const hasDrawerTransitions = isOverSmViewport && isOverMdViewport; const getDrawerContent = React.useCallback( - (viewport: "phone" | "tablet" | "desktop") => ( + (viewport: "phone" | "desktop") => ( - {getDrawerContent("tablet")} - - diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index bd024f9..8993084 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -1,10 +1,12 @@ -import { Dialog, Typography } from "@mui/material"; +import { Box, Button, ButtonGroup, Dialog, Typography } from "@mui/material"; import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; import type { Message, RoomEventsManager } from "../../utils/RoomEventsManager"; import { AccountIcon } from "./AccountIcon"; import React from "react"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; export function RoomMessagesList(p: { room: Room; @@ -67,6 +69,7 @@ function RoomMessage(p: { return ( <> + {/* Print date if required */} {p.firstMessageOfDay && ( )} + + {/* Give person name if required */} {(!p.previousFromSamePerson || p.firstMessageOfDay) && user && (
    )} -
    @@ -117,7 +132,26 @@ function RoomMessage(p: { ) : ( p.message.content )} -
    + + + + + Date: Fri, 28 Nov 2025 10:22:44 +0100 Subject: [PATCH 079/124] Can delete events from WebUI --- .../src/api/matrix/MatrixApiEvent.ts | 10 +++ .../src/widgets/messages/RoomMessagesList.tsx | 63 ++++++++++++++----- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index cd97463..f81e47d 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -91,4 +91,14 @@ export class MatrixApiEvent { jsonData: { content }, }); } + + /** + * Delete an event + */ + static async DeleteEvent(room: Room, event_id: string): Promise { + await APIClient.exec({ + method: "DELETE", + uri: `/matrix/room/${room.id}/event/${event_id}`, + }); + } } diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 8993084..9c5b058 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -1,12 +1,22 @@ -import { Box, Button, ButtonGroup, Dialog, Typography } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import EditIcon from "@mui/icons-material/Edit"; +import { + Box, + Button, + ButtonGroup, + Dialog, + Typography, + useTheme, +} from "@mui/material"; +import React from "react"; import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; +import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider"; +import { useConfirm } from "../../hooks/contexts_provider/ConfirmDialogProvider"; import type { Message, RoomEventsManager } from "../../utils/RoomEventsManager"; +import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; import { AccountIcon } from "./AccountIcon"; -import React from "react"; -import EditIcon from "@mui/icons-material/Edit"; -import DeleteIcon from "@mui/icons-material/Delete"; export function RoomMessagesList(p: { room: Room; @@ -61,11 +71,26 @@ function RoomMessage(p: { previousFromSamePerson: boolean; firstMessageOfDay: boolean; }): React.ReactElement { + const theme = useTheme(); + const user = useUserInfo(); + const alert = useAlert(); + const confirm = useConfirm(); + const [showImageFullScreen, setShowImageFullScreen] = React.useState(false); const closeImageFullScreen = () => setShowImageFullScreen(false); - const user = p.users.get(p.message.account); + const sender = p.users.get(p.message.account); + + const handleDeleteMessage = async () => { + if (!(await confirm(`Do you really want to delete this message?`))) return; + try { + await MatrixApiEvent.DeleteEvent(p.room, p.message.event_id); + } catch (e) { + console.error(`Failed to delete message!`, e), + alert(`Failed to delete message!${e}`); + } + }; return ( <> @@ -81,7 +106,7 @@ function RoomMessage(p: { )} {/* Give person name if required */} - {(!p.previousFromSamePerson || p.firstMessageOfDay) && user && ( + {(!p.previousFromSamePerson || p.firstMessageOfDay) && sender && (
    - +     - {user.display_name} + {sender.display_name}
    )} @@ -107,9 +132,12 @@ function RoomMessage(p: { }} component="div" sx={{ - "&:hover": { + [theme.getColorSchemeSelector("dark") + "&:hover"]: { backgroundColor: "#ffffff2b", }, + [theme.getColorSchemeSelector("light") + "&:hover"]: { + backgroundColor: "#00000039", + }, "&:hover *": { visibility: "visible" }, }} > @@ -142,14 +170,17 @@ function RoomMessage(p: { top: "-34px", right: "0px", }} - sx={{ "parent:hover": { visibility: "visible" } }} > - - + {p.message.account === user.info.matrix_user_id && ( + + )} + {p.message.account === user.info.matrix_user_id && ( + + )} From 93487a5325c8be25a4a5dc11c852bf3c7f3c4a44 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 10:41:01 +0100 Subject: [PATCH 080/124] Can edit message content --- .../src/api/matrix/MatrixApiEvent.ts | 15 ++++ .../src/widgets/messages/RoomMessagesList.tsx | 72 +++++++++++++++++-- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index f81e47d..5fdf43b 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -92,6 +92,21 @@ export class MatrixApiEvent { }); } + /** + * Edit text message content + */ + static async SetTextMessageContent( + room: Room, + event_id: string, + content: string + ): Promise { + await APIClient.exec({ + method: "POST", + uri: `/matrix/room/${room.id}/event/${event_id}/set_text_content`, + jsonData: { content }, + }); + } + /** * Delete an event */ diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 9c5b058..6133c06 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -5,6 +5,11 @@ import { Button, ButtonGroup, Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + TextField, Typography, useTheme, } from "@mui/material"; @@ -14,6 +19,7 @@ import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider"; import { useConfirm } from "../../hooks/contexts_provider/ConfirmDialogProvider"; +import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider"; import type { Message, RoomEventsManager } from "../../utils/RoomEventsManager"; import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; import { AccountIcon } from "./AccountIcon"; @@ -75,9 +81,12 @@ function RoomMessage(p: { const user = useUserInfo(); const alert = useAlert(); const confirm = useConfirm(); + const loadingMessage = useLoadingMessage(); const [showImageFullScreen, setShowImageFullScreen] = React.useState(false); + const [editMessage, setEditMessage] = React.useState(); + const closeImageFullScreen = () => setShowImageFullScreen(false); const sender = p.users.get(p.message.account); @@ -92,6 +101,27 @@ function RoomMessage(p: { } }; + const handleEditMessage = () => setEditMessage(p.message.content); + const handleCancelEditMessage = () => setEditMessage(undefined); + const handleSubmitEditMessage = async (event: React.FormEvent) => { + event.preventDefault(); + + try { + loadingMessage.show(`Updating message content...`); + await MatrixApiEvent.SetTextMessageContent( + p.room, + p.message.event_id, + editMessage! + ); + setEditMessage(undefined); + } catch (e) { + console.error(`Failed to edit message!`, e); + alert(`Failed to edit message content! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + return ( <> {/* Print date if required */} @@ -171,11 +201,12 @@ function RoomMessage(p: { right: "0px", }} > - {p.message.account === user.info.matrix_user_id && ( - - )} + {p.message.account === user.info.matrix_user_id && + !p.message.image && ( + + )} {p.message.account === user.info.matrix_user_id && ( + + +
    ); } From 756780513b4559a223c4a5e3e6c516f5f23f6ed7 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 10:41:30 +0100 Subject: [PATCH 081/124] Fix dialog name --- matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 6133c06..5fa5d7d 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -228,7 +228,7 @@ function RoomMessage(p: { {/* Edit message dialog */} - Subscribe + Edit message content Enter new message content:
    Date: Fri, 28 Nov 2025 11:41:19 +0100 Subject: [PATCH 082/124] Can send reaction from picker --- matrixgw_frontend/package-lock.json | 22 +++++++++++++ matrixgw_frontend/package.json | 1 + .../src/api/matrix/MatrixApiEvent.ts | 15 +++++++++ .../src/widgets/messages/RoomMessagesList.tsx | 33 +++++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index e91bab4..8da93cc 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -19,6 +19,7 @@ "@mui/x-date-pickers": "^8.17.0", "date-and-time": "^4.1.0", "dayjs": "^1.11.19", + "emoji-picker-react": "^4.16.1", "is-cidr": "^6.0.1", "qrcode.react": "^4.2.0", "react": "^19.1.1", @@ -2362,6 +2363,21 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-picker-react": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.16.1.tgz", + "integrity": "sha512-MrPX0tOCfRL3uYI4of/2GRZ7S6qS7YlacKiF78uFH84/C62vcuHE2DZyv5b4ZJMk0e06es1jjB4e31Bb+YSM8w==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -2680,6 +2696,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index 4d8f1b0..7beadb3 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -21,6 +21,7 @@ "@mui/x-date-pickers": "^8.17.0", "date-and-time": "^4.1.0", "dayjs": "^1.11.19", + "emoji-picker-react": "^4.16.1", "is-cidr": "^6.0.1", "qrcode.react": "^4.2.0", "react": "^19.1.1", diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index 5fdf43b..35bc1d6 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -107,6 +107,21 @@ export class MatrixApiEvent { }); } + /** + * React to event + */ + static async ReactToEvent( + room: Room, + event_id: string, + key: string + ): Promise { + await APIClient.exec({ + method: "POST", + uri: `/matrix/room/${room.id}/event/${event_id}/react`, + jsonData: { key }, + }); + } + /** * Delete an event */ diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 5fa5d7d..aeb29ce 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -1,3 +1,4 @@ +import AddReactionIcon from "@mui/icons-material/AddReaction"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; import { @@ -13,6 +14,7 @@ import { Typography, useTheme, } from "@mui/material"; +import EmojiPicker, { EmojiStyle, Theme } from "emoji-picker-react"; import React from "react"; import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; @@ -86,6 +88,7 @@ function RoomMessage(p: { const [showImageFullScreen, setShowImageFullScreen] = React.useState(false); const [editMessage, setEditMessage] = React.useState(); + const [pickReaction, setPickReaction] = React.useState(false); const closeImageFullScreen = () => setShowImageFullScreen(false); @@ -122,6 +125,21 @@ function RoomMessage(p: { } }; + const handleAddReaction = () => setPickReaction(true); + const handleCancelAddReaction = () => setPickReaction(false); + const handleSelectEmoji = async (key: string) => { + loadingMessage.show("Setting reaction..."); + try { + await MatrixApiEvent.ReactToEvent(p.room, p.message.event_id, key); + setPickReaction(false); + } catch (e) { + console.error("Failed to select emoji!", e); + alert(`Failed to select emoji! ${e}`); + } finally { + loadingMessage.hide(); + } + }; + return ( <> {/* Print date if required */} @@ -201,12 +219,18 @@ function RoomMessage(p: { right: "0px", }} > + {/* Add reaction */} + + {/* Edit text message */} {p.message.account === user.info.matrix_user_id && !p.message.image && ( )} + {/* Delete message */} {p.message.account === user.info.matrix_user_id && (
    + {/* Pick reaction dialog */} + + handleSelectEmoji(emoji.emoji)} + /> + + {/* Edit message dialog */} Edit message content From 9f0bc3303c9038ed855c0dee83d174e2bbe175c5 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 12:05:29 +0100 Subject: [PATCH 083/124] Add quick reactions --- .../src/widgets/messages/RoomMessagesList.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index aeb29ce..a912ada 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -219,6 +219,10 @@ function RoomMessage(p: { right: "0px", }} > + {/* Common reactions */} + {/* 👍 */} + {/* ♥️ */} + {/* 😂 */} {/* Add reaction */} ; +} From 6c11979ef25119dd145cf0f0ed4e9d591c9d38a0 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 14:37:17 +0100 Subject: [PATCH 084/124] Display reactions below messages --- .../src/utils/RoomEventsManager.ts | 12 ++- matrixgw_frontend/src/widgets/EmojiIcon.tsx | 24 ++++++ .../src/widgets/messages/RoomMessagesList.tsx | 81 +++++++++++++++++-- 3 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 matrixgw_frontend/src/widgets/EmojiIcon.tsx diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 5a8f72d..36e5a1a 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -17,7 +17,7 @@ export interface Message { time_sent: number; time_sent_dayjs: dayjs.Dayjs; modified: boolean; - reactions: MessageReaction[]; + reactions: Map; content: string; image?: string; } @@ -80,7 +80,7 @@ export class RoomEventsManager { event_id: evt.id, account: evt.sender, modified: false, - reactions: [], + reactions: new Map(), time_sent: evt.time, time_sent_dayjs: dayjs.unix(evt.time / 1000), image: data.content.file?.url, @@ -93,12 +93,16 @@ export class RoomEventsManager { const message = this.messages.find( (m) => m.event_id === data.content["m.relates_to"].event_id ); + const key = data.content["m.relates_to"].key; if (!message) continue; - message.reactions.push({ + + if (!message.reactions.has(key)) message.reactions.set(key, []); + + message.reactions.get(key)!.push({ account: evt.sender, event_id: evt.id, - key: data.content["m.relates_to"].key, + key, }); } } diff --git a/matrixgw_frontend/src/widgets/EmojiIcon.tsx b/matrixgw_frontend/src/widgets/EmojiIcon.tsx new file mode 100644 index 0000000..a51b780 --- /dev/null +++ b/matrixgw_frontend/src/widgets/EmojiIcon.tsx @@ -0,0 +1,24 @@ +import { Emoji, EmojiStyle } from "emoji-picker-react"; + +function emojiUnicode(emoji: string): string { + let comp; + if (emoji.length === 1) { + comp = emoji.charCodeAt(0); + } + comp = + (emoji.charCodeAt(0) - 0xd800) * 0x400 + + (emoji.charCodeAt(1) - 0xdc00) + + 0x10000; + if (comp < 0) { + comp = emoji.charCodeAt(0); + } + const s = comp.toString(16); + return s.includes("f") ? s : `${s}-fe0f`; +} + +export function EmojiIcon(p: { emojiKey: string }): React.ReactElement { + const unified = emojiUnicode(p.emojiKey); + return ( + + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index a912ada..a06349c 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -5,6 +5,7 @@ import { Box, Button, ButtonGroup, + Chip, Dialog, DialogActions, DialogContent, @@ -22,8 +23,14 @@ import type { Room } from "../../api/matrix/MatrixApiRoom"; import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider"; import { useConfirm } from "../../hooks/contexts_provider/ConfirmDialogProvider"; import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider"; -import type { Message, RoomEventsManager } from "../../utils/RoomEventsManager"; +import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider"; +import type { + Message, + MessageReaction, + RoomEventsManager, +} from "../../utils/RoomEventsManager"; import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; +import { EmojiIcon } from "../EmojiIcon"; import { AccountIcon } from "./AccountIcon"; export function RoomMessagesList(p: { @@ -83,6 +90,7 @@ function RoomMessage(p: { const user = useUserInfo(); const alert = useAlert(); const confirm = useConfirm(); + const snackbar = useSnackbar(); const loadingMessage = useLoadingMessage(); const [showImageFullScreen, setShowImageFullScreen] = React.useState(false); @@ -140,6 +148,20 @@ function RoomMessage(p: { } }; + const handleToggleReaction = async ( + key: string, + reaction: MessageReaction + ) => { + try { + if (!reaction) + await MatrixApiEvent.ReactToEvent(p.room, p.message.event_id, key); + else await MatrixApiEvent.DeleteEvent(p.room, reaction.event_id); + } catch (e) { + console.error(`Failed to toggle reaction!`, e); + snackbar(`Failed to toggle reaction! ${e}`); + } + }; + return ( <> {/* Print date if required */} @@ -243,6 +265,49 @@ function RoomMessage(p: { + {/* Reaction */} + + {[...p.message.reactions.keys()].map((r) => { + const userReaction = p.message.reactions + .get(r)! + .find((r) => r.account === user.info.matrix_user_id); + return ( + handleToggleReaction(r, userReaction) }, + label: { style: { height: "2em" } }, + }} + color={userReaction !== undefined ? "success" : undefined} + variant="filled" + label={ + +
    + +
    +
    + {p.message.reactions.get(r)?.length} +
    +
    + } + /> + ); + })} +
    + {/* Full screen image dialog */} r.key === p.emojiKey && r.account === user.info.matrix_user_id - ) !== undefined + p.message.reactions + .get(p.emojiKey) + ?.find( + (r) => r.key === p.emojiKey && r.account === user.info.matrix_user_id + ) !== undefined ) return <>; - return ; + return ( + + ); } From 799341f77c6acc237c8684368ca12b01a460a0f2 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 14:42:02 +0100 Subject: [PATCH 085/124] Display the list of people who reacted --- .../src/widgets/messages/RoomMessagesList.tsx | 85 +++++++++++-------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index a06349c..7b7d03c 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -12,6 +12,7 @@ import { DialogContentText, DialogTitle, TextField, + Tooltip, Typography, useTheme, } from "@mui/material"; @@ -150,7 +151,7 @@ function RoomMessage(p: { const handleToggleReaction = async ( key: string, - reaction: MessageReaction + reaction: MessageReaction | undefined ) => { try { if (!reaction) @@ -268,42 +269,58 @@ function RoomMessage(p: { {/* Reaction */} {[...p.message.reactions.keys()].map((r) => { - const userReaction = p.message.reactions - .get(r)! - .find((r) => r.account === user.info.matrix_user_id); + const reactions = p.message.reactions.get(r)!; + const userReaction = reactions.find( + (r) => r.account === user.info.matrix_user_id + ); return ( - handleToggleReaction(r, userReaction) }, - label: { style: { height: "2em" } }, - }} - color={userReaction !== undefined ? "success" : undefined} - variant="filled" - label={ - -
    - -
    -
    - {p.message.reactions.get(r)?.length} -
    + + {reactions + .map((r) => p.users.get(r.account)?.display_name) + .join("\n")}
    } - /> + > + handleToggleReaction(r, userReaction), + }, + label: { style: { height: "2em" } }, + }} + color={userReaction !== undefined ? "success" : undefined} + variant="filled" + label={ + +
    + +
    +
    + {reactions.length} +
    +
    + } + /> + ); })}
    From 4b30d67706ef6621928cae25a2cd505edf750400 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 17:00:42 +0100 Subject: [PATCH 086/124] Basic WS sync --- matrixgw_frontend/src/api/WsApi.ts | 53 +++++++++++++-- .../src/utils/RoomEventsManager.ts | 57 +++++++++++++++++ .../src/widgets/messages/MatrixWS.tsx | 64 +++++++++++++++++++ .../src/widgets/messages/RoomWidget.tsx | 10 +++ 4 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 matrixgw_frontend/src/widgets/messages/MatrixWS.tsx diff --git a/matrixgw_frontend/src/api/WsApi.ts b/matrixgw_frontend/src/api/WsApi.ts index 8560808..b213023 100644 --- a/matrixgw_frontend/src/api/WsApi.ts +++ b/matrixgw_frontend/src/api/WsApi.ts @@ -1,9 +1,54 @@ import { APIClient } from "./ApiClient"; -export type WsMessage = { - type: string; - [k: string]: any; -}; +interface BaseRoomEvent { + time: number; + room_id: string; + event_id: string; + sender: string; + origin_server_ts: number; +} + +type MessageType = "m.text" | "m.image" | string; + +export interface RoomMessageEvent extends BaseRoomEvent { + type: "RoomMessageEvent"; + data: { + msgtype: MessageType; + body: string; + "m.relates_to"?: { + rel_type?: "m.replace" | string; + event_id?: string; + }; + "m.new_content"?: { + msgtype?: MessageType; + body?: string; + }; + file?: { url: string }; + }; +} + +export interface RoomReactionEvent extends BaseRoomEvent { + type: "RoomReactionEvent"; + data: { + "m.relates_to": { + rel_type: string; + event_id: string; + key: string; + }; + }; +} + +export interface RoomRedactionEvent extends BaseRoomEvent { + type: "RoomRedactionEvent"; + data: { + redacts: string; + }; +} + +export type WsMessage = + | RoomMessageEvent + | RoomReactionEvent + | RoomRedactionEvent; export class WsApi { /** diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 36e5a1a..7b1621b 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -1,9 +1,11 @@ import dayjs from "dayjs"; import type { MatrixEvent, + MatrixEventData, MatrixEventsList, } from "../api/matrix/MatrixApiEvent"; import type { Room } from "../api/matrix/MatrixApiRoom"; +import type { WsMessage } from "../api/WsApi"; export interface MessageReaction { event_id: string; @@ -45,7 +47,62 @@ export class RoomEventsManager { this.rebuildMessagesList(); } + processWsMessage(m: WsMessage) { + if (m.room_id !== this.room.id) return false; + + let data: MatrixEventData; + if (m.type === "RoomReactionEvent") { + data = { + type: "m.reaction", + content: { + "m.relates_to": { + key: m.data["m.relates_to"].key, + event_id: m.data["m.relates_to"].event_id, + }, + }, + }; + } else if (m.type === "RoomRedactionEvent") { + data = { + type: "m.room.redaction", + redacts: m.data.redacts, + }; + } else if (m.type === "RoomMessageEvent") { + data = { + type: "m.room.message", + content: { + body: m.data["m.new_content"]?.body ?? m.data.body, + msgtype: m.data.msgtype, + "m.relates_to": + m.data["m.relates_to"] && m.data["m.relates_to"].event_id + ? { + event_id: m.data["m.relates_to"].event_id!, + rel_type: m.data["m.relates_to"].rel_type ?? "", + } + : undefined, + file: m.data.file, + }, + }; + } else { + // Ignore event + console.info("Event not supported => ignored"); + return false; + } + + this.events.push({ + sender: m.sender, + id: m.event_id, + time: m.origin_server_ts, + data, + }); + + this.rebuildMessagesList(); + + return true; + } + private rebuildMessagesList() { + this.messages = []; + // Sorts events list to process oldest events first this.events.sort((a, b) => a.time - b.time); diff --git a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx new file mode 100644 index 0000000..8d826de --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { WsApi, type WsMessage } from "../../api/WsApi"; +import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider"; +import CircleIcon from "@mui/icons-material/Circle"; +import { Tooltip } from "@mui/material"; + +const State = { + Closed: "Closed", + Connected: "Connected", + Error: "Error", +} as const; + +export function MatrixWS(p: { + onMessage: (msg: WsMessage) => void; +}): React.ReactElement { + const snackbar = useSnackbar(); + + const [state, setState] = React.useState(State.Closed); + const wsRef = React.useRef(undefined); + const [connCount, setConnCount] = React.useState(0); + + React.useEffect(() => { + const count = connCount; + const ws = new WebSocket(WsApi.WsURL); + wsRef.current = ws; + + ws.onopen = () => setState(State.Connected); + ws.onerror = (e) => { + if (count != connCount) return; + + console.error(`WS Debug error!`, e); + snackbar(`WebSocket error!`); + setState(State.Error); + + setTimeout(() => setConnCount(connCount + 1), 500); + }; + ws.onclose = () => { + setState(State.Closed); + wsRef.current = undefined; + }; + + ws.onmessage = (msg) => { + const dec = JSON.parse(msg.data); + console.info("WS message", dec); + p.onMessage(dec); + }; + + return () => ws.close(); + }, [connCount]); + + return ( + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index e990666..4888f63 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -2,8 +2,10 @@ import React from "react"; import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; +import type { WsMessage } from "../../api/WsApi"; import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { AsyncWidget } from "../AsyncWidget"; +import { MatrixWS } from "./MatrixWS"; import { RoomMessagesList } from "./RoomMessagesList"; import { SendMessageForm } from "./SendMessageForm"; @@ -11,6 +13,7 @@ export function RoomWidget(p: { room: Room; users: UsersMap; }): React.ReactElement { + const [_count, setCount] = React.useState(0); const [roomMgr, setRoomMgr] = React.useState(); const load = async () => { @@ -20,6 +23,10 @@ export function RoomWidget(p: { setRoomMgr(mgr); }; + const handleNewMessage = (m: WsMessage) => { + if (roomMgr?.processWsMessage(m)) setCount((c) => c + 1); + }; + return ( (
    +
    + +
    From 123e069d188fc3b69a2813be1eba64e9774aa66c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 17:15:33 +0100 Subject: [PATCH 087/124] Add multi line messages supports --- .../src/widgets/messages/RoomMessagesList.tsx | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 7b7d03c..d7b0b0f 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -44,7 +44,7 @@ export function RoomMessagesList(p: { // Automatically scroll to bottom when number of messages change React.useEffect(() => { if (messagesEndRef) - messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + messagesEndRef.current?.scrollIntoView({ behavior: "instant" }); }, [p.mgr.messages.length]); return ( @@ -200,6 +200,8 @@ function RoomMessage(p: { maxWidth: "100%", transition: "all 0.01s ease-in", position: "relative", + display: "flex", + flexDirection: "row", }} component="div" sx={{ @@ -214,23 +216,26 @@ function RoomMessage(p: { >   {p.message.time_sent_dayjs.format("HH:mm")} - {" "} -     - {p.message.image ? ( - setShowImageFullScreen(true)} - src={MatrixApiEvent.GetEventFileURL( - p.room, - p.message.event_id, - true - )} - style={{ - maxWidth: "200px", - }} - /> - ) : ( - p.message.content - )} + + + {/** Message itself */} +
    + {p.message.image ? ( + setShowImageFullScreen(true)} + src={MatrixApiEvent.GetEventFileURL( + p.room, + p.message.event_id, + true + )} + style={{ + maxWidth: "200px", + }} + /> + ) : ( + p.message.content + )} +
    setEditMessage(e.target.value)} /> From c36043291104a90b0b082fcf54c62072d6d9fd36 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 17:22:58 +0100 Subject: [PATCH 088/124] Add support for unencrypted media --- .../src/controllers/matrix/matrix_event_controller.rs | 4 +--- matrixgw_frontend/src/api/WsApi.ts | 1 + matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts | 1 + matrixgw_frontend/src/utils/RoomEventsManager.ts | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index f5eab4c..da2a1ee 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -216,9 +216,7 @@ pub async fn event_file( MessageType::Video(c) => (c.source(), c.thumbnail_source()), _ => (None, None), }; - - println!("{source:#?} {thumb_source:#?}"); - + let source = match (query.thumbnail, source, thumb_source) { (false, Some(s), _) => s, (true, _, Some(s)) => s, diff --git a/matrixgw_frontend/src/api/WsApi.ts b/matrixgw_frontend/src/api/WsApi.ts index b213023..04889be 100644 --- a/matrixgw_frontend/src/api/WsApi.ts +++ b/matrixgw_frontend/src/api/WsApi.ts @@ -23,6 +23,7 @@ export interface RoomMessageEvent extends BaseRoomEvent { msgtype?: MessageType; body?: string; }; + url?: string; file?: { url: string }; }; } diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index 35bc1d6..98d5404 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -10,6 +10,7 @@ export interface MatrixRoomMessage { event_id: string; rel_type: "m.replace" | string; }; + url?: string; file?: { url: string; }; diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 7b1621b..62db024 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -79,6 +79,7 @@ export class RoomEventsManager { rel_type: m.data["m.relates_to"].rel_type ?? "", } : undefined, + url: m.data.url, file: m.data.file, }, }; @@ -140,7 +141,7 @@ export class RoomEventsManager { reactions: new Map(), time_sent: evt.time, time_sent_dayjs: dayjs.unix(evt.time / 1000), - image: data.content.file?.url, + image: data.content.file?.url ?? data.content.url, content: data.content.body, }); } From 62966473f07748e4aa98ee0b9557029c4babdc09 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 17:37:30 +0100 Subject: [PATCH 089/124] Add support for more file formats --- matrixgw_frontend/src/api/WsApi.ts | 3 +- .../src/api/matrix/MatrixApiEvent.ts | 10 +++- .../src/utils/RoomEventsManager.ts | 7 ++- .../src/widgets/messages/RoomMessagesList.tsx | 54 +++++++++++++++++-- 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/matrixgw_frontend/src/api/WsApi.ts b/matrixgw_frontend/src/api/WsApi.ts index 04889be..23eb2f3 100644 --- a/matrixgw_frontend/src/api/WsApi.ts +++ b/matrixgw_frontend/src/api/WsApi.ts @@ -1,4 +1,5 @@ import { APIClient } from "./ApiClient"; +import type { MessageType } from "./matrix/MatrixApiEvent"; interface BaseRoomEvent { time: number; @@ -8,8 +9,6 @@ interface BaseRoomEvent { origin_server_ts: number; } -type MessageType = "m.text" | "m.image" | string; - export interface RoomMessageEvent extends BaseRoomEvent { type: "RoomMessageEvent"; data: { diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index 98d5404..f3d81f9 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -1,11 +1,19 @@ import { APIClient } from "../ApiClient"; import type { Room } from "./MatrixApiRoom"; +export type MessageType = + | "m.text" + | "m.image" + | "m.audio" + | "m.file" + | "m.video" + | "_OTHER_"; + export interface MatrixRoomMessage { type: "m.room.message"; content: { body: string; - msgtype: "m.text" | "m.image" | string; + msgtype: MessageType; "m.relates_to"?: { event_id: string; rel_type: "m.replace" | string; diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 62db024..1598c95 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -3,6 +3,7 @@ import type { MatrixEvent, MatrixEventData, MatrixEventsList, + MessageType, } from "../api/matrix/MatrixApiEvent"; import type { Room } from "../api/matrix/MatrixApiRoom"; import type { WsMessage } from "../api/WsApi"; @@ -21,7 +22,8 @@ export interface Message { modified: boolean; reactions: Map; content: string; - image?: string; + type: MessageType; + file?: string; } export class RoomEventsManager { @@ -141,7 +143,8 @@ export class RoomEventsManager { reactions: new Map(), time_sent: evt.time, time_sent_dayjs: dayjs.unix(evt.time / 1000), - image: data.content.file?.url ?? data.content.url, + type: data.content.msgtype, + file: data.content.file?.url ?? data.content.url, content: data.content.body, }); } diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index d7b0b0f..7658c2f 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -1,6 +1,7 @@ import AddReactionIcon from "@mui/icons-material/AddReaction"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; +import DownloadIcon from "@mui/icons-material/Download"; import { Box, Button, @@ -220,7 +221,8 @@ function RoomMessage(p: { {/** Message itself */}
    - {p.message.image ? ( + {/* Image */} + {p.message.type === "m.image" && ( setShowImageFullScreen(true)} src={MatrixApiEvent.GetEventFileURL( @@ -232,9 +234,53 @@ function RoomMessage(p: { maxWidth: "200px", }} /> - ) : ( - p.message.content )} + + {/* Audio */} + {p.message.type === "m.audio" && ( + + )} + + {/* Video */} + {p.message.type === "m.video" && ( + + )} + + {/* File */} + {p.message.type === "m.file" && ( + + + + )} + + {/* Text message */} + {p.message.type === "m.text" && p.message.content}
    {/* Edit text message */} {p.message.account === user.info.matrix_user_id && - !p.message.image && ( + !p.message.file && ( From d10c4d1a1c50ce6cdf4e99ab18b6144562ab9512 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 17:39:27 +0100 Subject: [PATCH 090/124] Minor appearance improvement --- matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 7658c2f..9070983 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -1,7 +1,7 @@ import AddReactionIcon from "@mui/icons-material/AddReaction"; import DeleteIcon from "@mui/icons-material/Delete"; -import EditIcon from "@mui/icons-material/Edit"; import DownloadIcon from "@mui/icons-material/Download"; +import EditIcon from "@mui/icons-material/Edit"; import { Box, Button, @@ -280,7 +280,9 @@ function RoomMessage(p: { )} {/* Text message */} - {p.message.type === "m.text" && p.message.content} + {p.message.type === "m.text" && ( +
    {p.message.content}
    + )}
    Date: Fri, 28 Nov 2025 18:06:40 +0100 Subject: [PATCH 091/124] Follow unread messages --- .../widgets/messages/MainMessagesWidget.tsx | 56 +++++++++++++++---- .../src/widgets/messages/MatrixWS.tsx | 14 +++-- .../src/widgets/messages/RoomWidget.tsx | 2 + 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index d8ec52a..c268ff2 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -6,7 +6,10 @@ import { } from "../../api/matrix/MatrixApiProfile"; import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom"; import { MatrixSyncApi } from "../../api/MatrixSyncApi"; +import type { WsMessage } from "../../api/WsApi"; import { AsyncWidget } from "../AsyncWidget"; +import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; +import { MatrixWS } from "./MatrixWS"; import { RoomSelector } from "./RoomSelector"; import { RoomWidget } from "./RoomWidget"; import { SpaceSelector } from "./SpaceSelector"; @@ -14,6 +17,7 @@ import { SpaceSelector } from "./SpaceSelector"; export function MainMessageWidget(): React.ReactElement { const [rooms, setRooms] = React.useState(); const [users, setUsers] = React.useState(); + const user = useUserInfo(); const load = async () => { await MatrixSyncApi.Start(); @@ -30,13 +34,39 @@ export function MainMessageWidget(): React.ReactElement { setUsers(await MatrixApiProfile.GetMultiple([...users])); }; + const handleEvent = (m: WsMessage) => { + // Add a new unread message + if ( + m.type === "RoomMessageEvent" && + !m.data["m.new_content"] && + m.sender !== user.info.matrix_user_id + ) { + setRooms((r) => { + const n = r ? [...r] : undefined; + const idx = n?.findIndex((el) => el.id === m.room_id); + if (idx) + n![idx] = { + ...n![idx], + number_unread_messages: n![idx].number_unread_messages + 1, + }; + return n; + }); + } + }; + return ( <_MainMessageWidget rooms={rooms!} users={users!} />} + build={() => ( + <_MainMessageWidget + rooms={rooms!} + users={users!} + onEvent={handleEvent} + /> + )} /> ); } @@ -44,6 +74,7 @@ export function MainMessageWidget(): React.ReactElement { function _MainMessageWidget(p: { rooms: Room[]; users: UsersMap; + onEvent: (m: WsMessage) => void; }): React.ReactElement { const [space, setSpace] = React.useState(); const [room, setRoom] = React.useState(); @@ -68,16 +99,19 @@ function _MainMessageWidget(p: { /> {room === undefined && ( -
    - No room selected. -
    + <> + +
    + No room selected. +
    + )} {room && }
    diff --git a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx index 8d826de..169c354 100644 --- a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx +++ b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx @@ -16,17 +16,17 @@ export function MatrixWS(p: { const snackbar = useSnackbar(); const [state, setState] = React.useState(State.Closed); - const wsRef = React.useRef(undefined); + const wsId = React.useRef(undefined); const [connCount, setConnCount] = React.useState(0); React.useEffect(() => { - const count = connCount; + const id = Math.random(); const ws = new WebSocket(WsApi.WsURL); - wsRef.current = ws; + wsId.current = id; ws.onopen = () => setState(State.Connected); ws.onerror = (e) => { - if (count != connCount) return; + if (wsId.current != id) return; console.error(`WS Debug error!`, e); snackbar(`WebSocket error!`); @@ -34,12 +34,16 @@ export function MatrixWS(p: { setTimeout(() => setConnCount(connCount + 1), 500); }; + ws.onclose = () => { + if (wsId.current !== id) return; setState(State.Closed); - wsRef.current = undefined; + wsId.current = undefined; }; ws.onmessage = (msg) => { + if (wsId.current !== id) return; + const dec = JSON.parse(msg.data); console.info("WS message", dec); p.onMessage(dec); diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index 4888f63..c231cce 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -12,6 +12,7 @@ import { SendMessageForm } from "./SendMessageForm"; export function RoomWidget(p: { room: Room; users: UsersMap; + onEvent: (m: WsMessage) => void; }): React.ReactElement { const [_count, setCount] = React.useState(0); const [roomMgr, setRoomMgr] = React.useState(); @@ -25,6 +26,7 @@ export function RoomWidget(p: { const handleNewMessage = (m: WsMessage) => { if (roomMgr?.processWsMessage(m)) setCount((c) => c + 1); + p.onEvent(m); }; return ( From 1f22d5c41b3441518206a2bd4986f6713715bb59 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 28 Nov 2025 18:41:43 +0100 Subject: [PATCH 092/124] Refactor rooms management --- .../src/utils/RoomEventsManager.ts | 5 +- .../widgets/messages/MainMessagesWidget.tsx | 132 ++++++++++++------ .../src/widgets/messages/MatrixWS.tsx | 8 +- .../src/widgets/messages/RoomMessagesList.tsx | 12 +- .../src/widgets/messages/RoomWidget.tsx | 40 +----- 5 files changed, 111 insertions(+), 86 deletions(-) diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 1598c95..5a4d041 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -50,7 +50,10 @@ export class RoomEventsManager { } processWsMessage(m: WsMessage) { - if (m.room_id !== this.room.id) return false; + if (m.room_id !== this.room.id) { + console.debug("Not an event for current room."); + return false; + } let data: MatrixEventData; if (m.type === "RoomReactionEvent") { diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index c268ff2..f6ac333 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -1,5 +1,6 @@ import { Divider } from "@mui/material"; import React from "react"; +import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import { MatrixApiProfile, type UsersMap, @@ -7,6 +8,7 @@ import { import { MatrixApiRoom, type Room } from "../../api/matrix/MatrixApiRoom"; import { MatrixSyncApi } from "../../api/MatrixSyncApi"; import type { WsMessage } from "../../api/WsApi"; +import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { AsyncWidget } from "../AsyncWidget"; import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; import { MatrixWS } from "./MatrixWS"; @@ -17,9 +19,8 @@ import { SpaceSelector } from "./SpaceSelector"; export function MainMessageWidget(): React.ReactElement { const [rooms, setRooms] = React.useState(); const [users, setUsers] = React.useState(); - const user = useUserInfo(); - const load = async () => { + const loadRoomsList = async () => { await MatrixSyncApi.Start(); const rooms = await MatrixApiRoom.ListJoined(); @@ -34,37 +35,17 @@ export function MainMessageWidget(): React.ReactElement { setUsers(await MatrixApiProfile.GetMultiple([...users])); }; - const handleEvent = (m: WsMessage) => { - // Add a new unread message - if ( - m.type === "RoomMessageEvent" && - !m.data["m.new_content"] && - m.sender !== user.info.matrix_user_id - ) { - setRooms((r) => { - const n = r ? [...r] : undefined; - const idx = n?.findIndex((el) => el.id === m.room_id); - if (idx) - n![idx] = { - ...n![idx], - number_unread_messages: n![idx].number_unread_messages + 1, - }; - return n; - }); - } - }; - return ( ( <_MainMessageWidget rooms={rooms!} users={users!} - onEvent={handleEvent} + onRoomsListUpdate={(cb) => setRooms((r) => cb(r!))} /> )} /> @@ -74,10 +55,12 @@ export function MainMessageWidget(): React.ReactElement { function _MainMessageWidget(p: { rooms: Room[]; users: UsersMap; - onEvent: (m: WsMessage) => void; + onRoomsListUpdate: (cb: (a: Room[]) => Room[]) => void; }): React.ReactElement { + const user = useUserInfo(); + const [space, setSpace] = React.useState(); - const [room, setRoom] = React.useState(); + const [currentRoom, setCurrentRoom] = React.useState(); const spaceRooms = React.useMemo(() => { return p.rooms @@ -87,33 +70,96 @@ function _MainMessageWidget(p: { ); }, [space, p.rooms]); + const [_refreshCount, setRefreshCount] = React.useState(0); + const [roomMgr, setRoomMgr] = React.useState(); + + const loadRoom = async () => { + setRoomMgr(undefined); + if (!currentRoom) { + console.warn("Cannot load manager for no room!"); + return; + } + const messages = await MatrixApiEvent.GetRoomEvents(currentRoom); + const mgr = new RoomEventsManager(currentRoom!, messages); + setRoomMgr(mgr); + }; + + const handleWsEvent = (m: WsMessage) => { + // Process messages for current room + if (roomMgr?.processWsMessage(m)) { + console.info("Current room updated!"); + setRefreshCount((c) => c + 1); + } + + // Add a new unread message on left sidebar + if ( + m.type === "RoomMessageEvent" && + !m.data["m.new_content"] && + m.sender !== user.info.matrix_user_id + ) { + p.onRoomsListUpdate((r) => { + const n = [...r]; + const idx = r.findIndex((el) => el.id === m.room_id); + if (idx) + n[idx] = { + ...n[idx], + number_unread_messages: n[idx].number_unread_messages + 1, + }; + return n; + }); + } + }; + return (
    + {/* Websocket */} +
    + +
    + + {/* Space selector */} + + {/* Separator */} + + {/* Room selector */} + + {/* Separator */} - {room === undefined && ( - <> - -
    - No room selected. -
    - + + {/* If no room is selected */} + {currentRoom === undefined && ( +
    + No room selected. +
    + )} + + {/* In case of room */} + {currentRoom && ( + ( + + )} + /> )} - {room && }
    ); } diff --git a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx index 169c354..f4d4833 100644 --- a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx +++ b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx @@ -15,6 +15,12 @@ export function MatrixWS(p: { }): React.ReactElement { const snackbar = useSnackbar(); + // Keep only the latest version of onMessage + const cbRef = React.useRef(p.onMessage); + React.useEffect(() => { + cbRef.current = p.onMessage; + }, [p.onMessage]); + const [state, setState] = React.useState(State.Closed); const wsId = React.useRef(undefined); const [connCount, setConnCount] = React.useState(0); @@ -46,7 +52,7 @@ export function MatrixWS(p: { const dec = JSON.parse(msg.data); console.info("WS message", dec); - p.onMessage(dec); + cbRef.current(dec); }; return () => ws.close(); diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 9070983..480adc9 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -38,7 +38,7 @@ import { AccountIcon } from "./AccountIcon"; export function RoomMessagesList(p: { room: Room; users: UsersMap; - mgr: RoomEventsManager; + manager: RoomEventsManager; }): React.ReactElement { const messagesEndRef = React.createRef(); @@ -46,7 +46,7 @@ export function RoomMessagesList(p: { React.useEffect(() => { if (messagesEndRef) messagesEndRef.current?.scrollIntoView({ behavior: "instant" }); - }, [p.mgr.messages.length]); + }, [p.manager.messages.length]); return (
    - {p.mgr.messages.map((m, idx) => ( + {p.manager.messages.map((m, idx) => ( 0 && - p.mgr.messages[idx - 1].account === m.account && - m.time_sent - p.mgr.messages[idx - 1].time_sent < 60 * 3 * 1000 + p.manager.messages[idx - 1].account === m.account && + m.time_sent - p.manager.messages[idx - 1].time_sent < 60 * 3 * 1000 } firstMessageOfDay={ idx === 0 || m.time_sent_dayjs.startOf("day").unix() != - p.mgr.messages[idx - 1].time_sent_dayjs.startOf("day").unix() + p.manager.messages[idx - 1].time_sent_dayjs.startOf("day").unix() } /> ))} diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index c231cce..77d52f1 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -1,49 +1,19 @@ import React from "react"; -import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; -import type { WsMessage } from "../../api/WsApi"; import { RoomEventsManager } from "../../utils/RoomEventsManager"; -import { AsyncWidget } from "../AsyncWidget"; -import { MatrixWS } from "./MatrixWS"; import { RoomMessagesList } from "./RoomMessagesList"; import { SendMessageForm } from "./SendMessageForm"; export function RoomWidget(p: { room: Room; users: UsersMap; - onEvent: (m: WsMessage) => void; + manager: RoomEventsManager; }): React.ReactElement { - const [_count, setCount] = React.useState(0); - const [roomMgr, setRoomMgr] = React.useState(); - - const load = async () => { - setRoomMgr(undefined); - const messages = await MatrixApiEvent.GetRoomEvents(p.room); - const mgr = new RoomEventsManager(p.room, messages); - setRoomMgr(mgr); - }; - - const handleNewMessage = (m: WsMessage) => { - if (roomMgr?.processWsMessage(m)) setCount((c) => c + 1); - p.onEvent(m); - }; - return ( - ( -
    -
    - -
    - - -
    - )} - /> +
    + + +
    ); } From 64985bb39e5c3c6efa25b4f106e3fe2d5b1c7826 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 08:36:29 +0100 Subject: [PATCH 093/124] Display application icon --- .../matrix/matrix_event_controller.rs | 2 +- matrixgw_frontend/package-lock.json | 16 ++++++++++++ matrixgw_frontend/package.json | 1 + .../src/widgets/messages/AppIconModifier.tsx | 26 +++++++++++++++++++ .../widgets/messages/MainMessagesWidget.tsx | 9 +++++++ 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index da2a1ee..efbe9e0 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -216,7 +216,7 @@ pub async fn event_file( MessageType::Video(c) => (c.source(), c.thumbnail_source()), _ => (None, None), }; - + let source = match (query.thumbnail, source, thumb_source) { (false, Some(s), _) => s, (true, _, Some(s)) => s, diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index 8da93cc..254aaa2 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -24,6 +24,7 @@ "qrcode.react": "^4.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-favicon": "^2.0.7", "react-json-view-lite": "^2.5.0", "react-router": "^7.9.5" }, @@ -3691,6 +3692,21 @@ "react": "^19.2.0" } }, + "node_modules/react-favicon": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/react-favicon/-/react-favicon-2.0.7.tgz", + "integrity": "sha512-Vqjk8VHnOu7vl7JnP13nPJ05DyFGObF655xFkbTUIbF4vqLx1Slbc56Hrbzg1leROAKHzElm3u4KUzaXO46A6A==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-is": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index 7beadb3..fb5008d 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -26,6 +26,7 @@ "qrcode.react": "^4.2.0", "react": "^19.1.1", "react-dom": "^19.1.1", + "react-favicon": "^2.0.7", "react-json-view-lite": "^2.5.0", "react-router": "^7.9.5" }, diff --git a/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx b/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx new file mode 100644 index 0000000..4d10089 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx @@ -0,0 +1,26 @@ +import Favicon from "react-favicon"; + +// Taken from https://github.com/element-hq/element-web/blob/0577e245dac944bd85eea07b93a9762a93062f62/src/favicon.ts +function getInitialFavicon(): HTMLLinkElement[] { + const icons: HTMLLinkElement[] = []; + const links = window.document + .getElementsByTagName("head")[0] + .getElementsByTagName("link"); + for (const link of links) { + if ( + link.hasAttribute("rel") && + /(^|\s)icon(\s|$)/i.test(link.getAttribute("rel")!) + ) { + icons.push(link); + } + } + return icons; +} + +let iconPath = getInitialFavicon()[0].getAttribute("href")!; + +export function AppIconModifier(p: { + numberUnread: number; +}): React.ReactElement { + return ; +} diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index f6ac333..8d87f46 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -11,6 +11,7 @@ import type { WsMessage } from "../../api/WsApi"; import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { AsyncWidget } from "../AsyncWidget"; import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; +import { AppIconModifier } from "./AppIconModifier"; import { MatrixWS } from "./MatrixWS"; import { RoomSelector } from "./RoomSelector"; import { RoomWidget } from "./RoomWidget"; @@ -70,6 +71,11 @@ function _MainMessageWidget(p: { ); }, [space, p.rooms]); + const unreadRooms = React.useMemo( + () => p.rooms.filter((r) => r.number_unread_messages > 0).length, + [p.rooms] + ); + const [_refreshCount, setRefreshCount] = React.useState(0); const [roomMgr, setRoomMgr] = React.useState(); @@ -117,6 +123,9 @@ function _MainMessageWidget(p: {
    + {/** Application icon modifier */} + + {/* Space selector */} From 7acb0cbafa67bb6c29272058c62abd533da6867b Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 08:44:30 +0100 Subject: [PATCH 094/124] Display WS state in favicon --- .../src/widgets/messages/AppIconModifier.tsx | 11 ++++++- .../widgets/messages/MainMessagesWidget.tsx | 5 +-- .../src/widgets/messages/MatrixWS.tsx | 33 +++++++++++++++---- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx b/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx index 4d10089..5cce966 100644 --- a/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx +++ b/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx @@ -1,4 +1,5 @@ import Favicon from "react-favicon"; +import { WSState } from "./MatrixWS"; // Taken from https://github.com/element-hq/element-web/blob/0577e245dac944bd85eea07b93a9762a93062f62/src/favicon.ts function getInitialFavicon(): HTMLLinkElement[] { @@ -21,6 +22,14 @@ let iconPath = getInitialFavicon()[0].getAttribute("href")!; export function AppIconModifier(p: { numberUnread: number; + state: string; }): React.ReactElement { - return ; + const isError = p.state === WSState.Error || p.state === WSState.Closed; + return ( + + ); } diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index 8d87f46..1980e8f 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -90,6 +90,7 @@ function _MainMessageWidget(p: { setRoomMgr(mgr); }; + const [wsState, setWsState] = React.useState(""); const handleWsEvent = (m: WsMessage) => { // Process messages for current room if (roomMgr?.processWsMessage(m)) { @@ -120,11 +121,11 @@ function _MainMessageWidget(p: {
    {/* Websocket */}
    - +
    {/** Application icon modifier */} - + {/* Space selector */} diff --git a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx index f4d4833..8e376cb 100644 --- a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx +++ b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx @@ -4,7 +4,7 @@ import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider"; import CircleIcon from "@mui/icons-material/Circle"; import { Tooltip } from "@mui/material"; -const State = { +export const WSState = { Closed: "Closed", Connected: "Connected", Error: "Error", @@ -12,6 +12,7 @@ const State = { export function MatrixWS(p: { onMessage: (msg: WsMessage) => void; + onStateChange?: (state: string) => void; }): React.ReactElement { const snackbar = useSnackbar(); @@ -21,7 +22,13 @@ export function MatrixWS(p: { cbRef.current = p.onMessage; }, [p.onMessage]); - const [state, setState] = React.useState(State.Closed); + // Keep only the latest version of onStateChange + const stateCbRef = React.useRef(p.onStateChange); + React.useEffect(() => { + stateCbRef.current = p.onStateChange; + }, [p.onStateChange]); + + const [state, setState] = React.useState(WSState.Closed); const wsId = React.useRef(undefined); const [connCount, setConnCount] = React.useState(0); @@ -30,23 +37,35 @@ export function MatrixWS(p: { const ws = new WebSocket(WsApi.WsURL); wsId.current = id; - ws.onopen = () => setState(State.Connected); + // Open + ws.onopen = () => { + if (wsId.current != id) return; + + setState(WSState.Connected); + stateCbRef.current?.(WSState.Connected); + }; + + // Error ws.onerror = (e) => { if (wsId.current != id) return; console.error(`WS Debug error!`, e); snackbar(`WebSocket error!`); - setState(State.Error); + setState(WSState.Error); + stateCbRef.current?.(WSState.Error); setTimeout(() => setConnCount(connCount + 1), 500); }; + // Close ws.onclose = () => { if (wsId.current !== id) return; - setState(State.Closed); + setState(WSState.Closed); + stateCbRef.current?.(WSState.Closed); wsId.current = undefined; }; + // Message ws.onmessage = (msg) => { if (wsId.current !== id) return; @@ -62,9 +81,9 @@ export function MatrixWS(p: { Date: Mon, 1 Dec 2025 09:12:58 +0100 Subject: [PATCH 095/124] Can check if rooms are muted --- .../matrix/matrix_room_controller.rs | 44 ++++++++++++++----- .../src/api/matrix/MatrixApiRoom.ts | 1 + .../widgets/messages/MainMessagesWidget.tsx | 5 ++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index 1886169..c106eca 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -4,6 +4,9 @@ use crate::controllers::matrix::matrix_media_controller; use crate::extractors::matrix_client_extractor::MatrixClientExtractor; use actix_web::{HttpRequest, HttpResponse, web}; use futures_util::{StreamExt, stream}; +use matrix_sdk::notification_settings::{ + IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode, +}; use matrix_sdk::room::ParentSpace; use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId}; use matrix_sdk::{Room, RoomMemberships}; @@ -17,11 +20,12 @@ pub struct APIRoomInfo { is_space: bool, parents: Vec, number_unread_messages: u64, + notifications: RoomNotificationMode, latest_event: Option, } impl APIRoomInfo { - async fn from_room(r: &Room) -> anyhow::Result { + async fn from_room(r: &Room, notif: &NotificationSettings) -> anyhow::Result { // Get parent spaces let parent_spaces = r .parent_spaces() @@ -39,19 +43,34 @@ impl APIRoomInfo { }) .collect::>(); + let members = r + .members(RoomMemberships::ACTIVE) + .await? + .into_iter() + .map(|r| r.user_id().to_owned()) + .collect::>(); + + let notifications = notif + .get_user_defined_room_notification_mode(r.room_id()) + .await + .unwrap_or( + notif + .get_default_room_notification_mode( + IsEncrypted::from(r.encryption_state().is_encrypted()), + IsOneToOne::from(members.len() == 2), + ) + .await, + ); + Ok(Self { id: r.room_id().to_owned(), name: r.name(), - members: r - .members(RoomMemberships::ACTIVE) - .await? - .into_iter() - .map(|r| r.user_id().to_owned()) - .collect::>(), + members, avatar: r.avatar_url(), is_space: r.is_space(), parents: parent_spaces, number_unread_messages: r.unread_notification_counts().notification_count, + notifications, latest_event: get_events(r, 1, None).await?.events.into_iter().next(), }) } @@ -59,8 +78,9 @@ impl APIRoomInfo { /// Get the list of joined rooms of the user pub async fn joined_rooms(client: MatrixClientExtractor) -> HttpResult { + let notifs = client.client.client.notification_settings().await; let list = stream::iter(client.client.client.joined_rooms()) - .then(async |room| APIRoomInfo::from_room(&room).await) + .then(async |room| APIRoomInfo::from_room(&room, ¬ifs).await) .collect::>() .await .into_iter() @@ -71,8 +91,10 @@ pub async fn joined_rooms(client: MatrixClientExtractor) -> HttpResult { /// Get joined spaces rooms of user pub async fn get_joined_spaces(client: MatrixClientExtractor) -> HttpResult { + let notifs = client.client.client.notification_settings().await; + let list = stream::iter(client.client.client.joined_space_rooms()) - .then(async |room| APIRoomInfo::from_room(&room).await) + .then(async |room| APIRoomInfo::from_room(&room, ¬ifs).await) .collect::>() .await .into_iter() @@ -91,9 +113,11 @@ pub async fn single_room_info( client: MatrixClientExtractor, path: web::Path, ) -> HttpResult { + let notifs = client.client.client.notification_settings().await; + Ok(match client.client.client.get_room(&path.room_id) { None => HttpResponse::NotFound().json("Room not found"), - Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r).await?), + Some(r) => HttpResponse::Ok().json(APIRoomInfo::from_room(&r, ¬ifs).await?), }) } diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts index fecbd21..8cfbcbc 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts @@ -11,6 +11,7 @@ export interface Room { is_space?: boolean; parents: string[]; number_unread_messages: number; + notifications: "AllMessages" | "MentionsAndKeywordsOnly" | "Mute"; latest_event?: MatrixEvent; } diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index 1980e8f..8875faf 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -72,7 +72,10 @@ function _MainMessageWidget(p: { }, [space, p.rooms]); const unreadRooms = React.useMemo( - () => p.rooms.filter((r) => r.number_unread_messages > 0).length, + () => + p.rooms.filter( + (r) => r.number_unread_messages > 0 && r.notifications === "AllMessages" + ).length, [p.rooms] ); From b93100413c113b24d8652655579682f92e68dd50 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 10:12:11 +0100 Subject: [PATCH 096/124] Propagate read receipt events --- matrixgw_backend/src/broadcast_messages.rs | 3 ++ .../src/controllers/ws_controller.rs | 35 +++++++++++++++++++ .../src/matrix_connection/sync_thread.rs | 17 ++++++++- matrixgw_frontend/src/api/WsApi.ts | 16 ++++++++- 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs index db34a75..2e129eb 100644 --- a/matrixgw_backend/src/broadcast_messages.rs +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -2,6 +2,7 @@ use crate::matrix_connection::sync_thread::MatrixSyncTaskID; use crate::users::{APIToken, UserEmail}; use matrix_sdk::Room; use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent; +use matrix_sdk::ruma::events::receipt::SyncReceiptEvent; use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent; use matrix_sdk::ruma::events::room::redaction::OriginalSyncRoomRedactionEvent; use matrix_sdk::sync::SyncResponse; @@ -32,6 +33,8 @@ pub enum BroadcastMessage { ReactionEvent(BxRoomEvent), /// New room redaction RoomRedactionEvent(BxRoomEvent), + /// Message fully read event + ReceiptEvent(BxRoomEvent), /// Raw Matrix sync response MatrixSyncResponse { user: UserEmail, sync: SyncResponse }, } diff --git a/matrixgw_backend/src/controllers/ws_controller.rs b/matrixgw_backend/src/controllers/ws_controller.rs index 0c0fab1..62553e7 100644 --- a/matrixgw_backend/src/controllers/ws_controller.rs +++ b/matrixgw_backend/src/controllers/ws_controller.rs @@ -29,6 +29,19 @@ pub struct WsRoomEvent { pub data: Box, } +#[derive(Debug, serde::Serialize)] +pub struct WsReceiptEntry { + event: OwnedEventId, + user: OwnedUserId, + ts: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct WsReceiptEvent { + pub room_id: OwnedRoomId, + pub receipts: Vec, +} + /// Messages sent to the client #[derive(Debug, serde::Serialize)] #[serde(tag = "type")] @@ -41,6 +54,9 @@ pub enum WsMessage { /// Room reaction event RoomRedactionEvent(WsRoomEvent), + + /// Fully read message event + ReceiptEvent(WsReceiptEvent), } impl WsMessage { @@ -73,6 +89,25 @@ impl WsMessage { data: Box::new(evt.data.content.clone()), })) } + BroadcastMessage::ReceiptEvent(evt) if &evt.user == user => { + let mut receipts = vec![]; + for (event_id, r) in &evt.data.content.0 { + for user_receipts in r.values() { + for (user, receipt) in user_receipts { + receipts.push(WsReceiptEntry { + event: event_id.clone(), + user: user.clone(), + ts: receipt.ts, + }) + } + } + } + + Some(Self::ReceiptEvent(WsReceiptEvent { + room_id: evt.room.room_id().to_owned(), + receipts, + })) + } _ => None, } } diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs index 30f4307..439c753 100644 --- a/matrixgw_backend/src/matrix_connection/sync_thread.rs +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -8,6 +8,7 @@ use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use futures_util::StreamExt; use matrix_sdk::Room; use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent; +use matrix_sdk::ruma::events::receipt::SyncReceiptEvent; use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent; use matrix_sdk::ruma::events::room::redaction::OriginalSyncRoomRedactionEvent; use ractor::ActorRef; @@ -91,11 +92,25 @@ async fn sync_thread_task( room, })) { - log::warn!("Failed to forward reaction event! {e}"); + log::warn!("Failed to forward redaction event! {e}"); } }, )); + let tx_receipt_handle = tx.clone(); + let user_receipt_handle = client.email.clone(); + handlers.push( + client.add_event_handler(async move |event: SyncReceiptEvent, room: Room| { + if let Err(e) = tx_receipt_handle.send(BroadcastMessage::ReceiptEvent(BxRoomEvent { + user: user_receipt_handle.clone(), + data: Box::new(event), + room, + })) { + log::warn!("Failed to forward receipt event! {e}"); + } + }), + ); + loop { tokio::select! { // Message from tokio broadcast diff --git a/matrixgw_frontend/src/api/WsApi.ts b/matrixgw_frontend/src/api/WsApi.ts index 23eb2f3..6104170 100644 --- a/matrixgw_frontend/src/api/WsApi.ts +++ b/matrixgw_frontend/src/api/WsApi.ts @@ -45,10 +45,24 @@ export interface RoomRedactionEvent extends BaseRoomEvent { }; } +export interface ReceiptEventEntry { + event: string; + user: string; + ts?: number; +} + +export interface RoomReceiptEvent { + time: number; + type: "ReceiptEvent"; + room_id: string; + receipts: ReceiptEventEntry[]; +} + export type WsMessage = | RoomMessageEvent | RoomReactionEvent - | RoomRedactionEvent; + | RoomRedactionEvent + | RoomReceiptEvent; export class WsApi { /** From 196671d0fbaf3b4dae77cd941589ccb14ac25015 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 10:25:14 +0100 Subject: [PATCH 097/124] Remove unread marker when receiving proper read receipt --- .../src/widgets/messages/MainMessagesWidget.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index 8875faf..9fdc242 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -118,6 +118,23 @@ function _MainMessageWidget(p: { return n; }); } + + // Remove unread message on left sidebar + if ( + m.type === "ReceiptEvent" && + m.receipts.find((r) => r.user === user.info.matrix_user_id) !== undefined + ) { + p.onRoomsListUpdate((r) => { + const n = [...r]; + const idx = r.findIndex((el) => el.id === m.room_id); + if (idx) + n[idx] = { + ...n[idx], + number_unread_messages: 0, + }; + return n; + }); + } }; return ( From 849aef9343f5d654765daf47229ed9e4ef30dc1c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 10:30:54 +0100 Subject: [PATCH 098/124] Update unread messages count only if room is not muted --- matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index 9fdc242..05bc37f 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -110,7 +110,7 @@ function _MainMessageWidget(p: { p.onRoomsListUpdate((r) => { const n = [...r]; const idx = r.findIndex((el) => el.id === m.room_id); - if (idx) + if (idx && n[idx].notifications === "AllMessages") n[idx] = { ...n[idx], number_unread_messages: n[idx].number_unread_messages + 1, From 9359dc5be05bd2d7587fa5df4d3e755d61f5a8ff Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 10:42:19 +0100 Subject: [PATCH 099/124] Send read receipts --- .../matrix/matrix_event_controller.rs | 22 ++++++++++++++++ matrixgw_backend/src/main.rs | 4 +++ .../src/api/matrix/MatrixApiEvent.ts | 10 ++++++++ .../src/widgets/messages/RoomWidget.tsx | 25 ++++++++++++++++++- 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index efbe9e0..94e4eb1 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -11,7 +11,9 @@ use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind}; use matrix_sdk::media::MediaEventContent; use matrix_sdk::room::MessagesOptions; use matrix_sdk::room::edit::EditedContent; +use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType; use matrix_sdk::ruma::events::reaction::ReactionEventContent; +use matrix_sdk::ruma::events::receipt::ReceiptThread; use matrix_sdk::ruma::events::relation::Annotation; use matrix_sdk::ruma::events::room::message::{ MessageType, RoomMessageEvent, RoomMessageEventContent, RoomMessageEventContentWithoutRelation, @@ -266,3 +268,23 @@ pub async fn redact_event( } }) } + +/// Send receipt for event +pub async fn receipt( + client: MatrixClientExtractor, + path: web::Path, + event_path: web::Path, +) -> HttpResult { + let Some(room) = client.client.client.get_room(&path.room_id) else { + return Ok(HttpResponse::NotFound().json("Room not found")); + }; + + room.send_single_receipt( + ReceiptType::Read, + ReceiptThread::default(), + event_path.event_id.clone(), + ) + .await?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 7623f65..1713864 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -188,6 +188,10 @@ async fn main() -> std::io::Result<()> { .route( "/api/matrix/room/{room_id}/event/{event_id}", web::delete().to(matrix_event_controller::redact_event), + ) + .route( + "/api/matrix/room/{room_id}/event/{event_id}/receipt", + web::post().to(matrix_event_controller::receipt), ) // Matrix media controller .route( diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index f3d81f9..497dc0d 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -140,4 +140,14 @@ export class MatrixApiEvent { uri: `/matrix/room/${room.id}/event/${event_id}`, }); } + + /** + * Send event receipt + */ + static async SendReceipt(room: Room, event_id: string): Promise { + await APIClient.exec({ + method: "POST", + uri: `/matrix/room/${room.id}/event/${event_id}/receipt`, + }); + } } diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index 77d52f1..61d33be 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -4,14 +4,37 @@ import type { Room } from "../../api/matrix/MatrixApiRoom"; import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { RoomMessagesList } from "./RoomMessagesList"; import { SendMessageForm } from "./SendMessageForm"; +import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; +import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider"; export function RoomWidget(p: { room: Room; users: UsersMap; manager: RoomEventsManager; }): React.ReactElement { + const snackbar = useSnackbar(); + + const receiptId = React.useRef(undefined); + + const handleRoomClick = async () => { + if (p.manager.messages.length === 0) return; + const latest = p.manager.messages[p.manager.messages.length - 1]; + if (latest.event_id === receiptId.current) return; + + receiptId.current = latest.event_id; + try { + await MatrixApiEvent.SendReceipt(p.room, latest.event_id); + } catch (e) { + console.error("Failed to send read receipt!", e); + snackbar(`Failed to send read receipt! ${e}`); + } + }; + return ( -
    +
    From dac20f60e06e1d951561d40a56fef4441a52f5e1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 11:09:14 +0100 Subject: [PATCH 100/124] Can get read receipts --- .../matrix/matrix_event_controller.rs | 20 +++++++-- .../matrix/matrix_room_controller.rs | 45 ++++++++++++++++++- matrixgw_backend/src/main.rs | 6 ++- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs index 94e4eb1..93baf63 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -11,6 +11,7 @@ use matrix_sdk::deserialized_responses::{TimelineEvent, TimelineEventKind}; use matrix_sdk::media::MediaEventContent; use matrix_sdk::room::MessagesOptions; use matrix_sdk::room::edit::EditedContent; +use matrix_sdk::ruma::api::client::filter::RoomEventFilter; use matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType; use matrix_sdk::ruma::events::reaction::ReactionEventContent; use matrix_sdk::ruma::events::receipt::ReceiptThread; @@ -25,7 +26,7 @@ use serde_json::value::RawValue; #[derive(Serialize)] pub struct APIEvent { - id: OwnedEventId, + pub id: OwnedEventId, time: MilliSecondsSinceUnixEpoch, sender: OwnedUserId, data: Box, @@ -63,10 +64,14 @@ pub(super) async fn get_events( room: &Room, limit: u32, from: Option<&str>, + filter: Option, ) -> anyhow::Result { let mut msg_opts = MessagesOptions::backward(); msg_opts.from = from.map(str::to_string); msg_opts.limit = UInt::from(limit); + if let Some(filter) = filter { + msg_opts.filter = filter; + } let messages = room.messages(msg_opts).await?; Ok(APIEventsList { @@ -99,8 +104,15 @@ pub async fn get_for_room( return Ok(HttpResponse::NotFound().json("Room not found!")); }; - Ok(HttpResponse::Ok() - .json(get_events(&room, query.limit.unwrap_or(500), query.from.as_deref()).await?)) + Ok(HttpResponse::Ok().json( + get_events( + &room, + query.limit.unwrap_or(500), + query.from.as_deref(), + None, + ) + .await?, + )) } #[derive(Deserialize)] @@ -281,7 +293,7 @@ pub async fn receipt( room.send_single_receipt( ReceiptType::Read, - ReceiptThread::default(), + ReceiptThread::Main, event_path.event_id.clone(), ) .await?; diff --git a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs index c106eca..cd63c9b 100644 --- a/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -8,7 +8,10 @@ use matrix_sdk::notification_settings::{ IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode, }; use matrix_sdk::room::ParentSpace; -use matrix_sdk::ruma::{OwnedMxcUri, OwnedRoomId, OwnedUserId}; +use matrix_sdk::ruma::events::receipt::{ReceiptThread, ReceiptType}; +use matrix_sdk::ruma::{ + MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedMxcUri, OwnedRoomId, OwnedUserId, +}; use matrix_sdk::{Room, RoomMemberships}; #[derive(serde::Serialize)] @@ -71,7 +74,11 @@ impl APIRoomInfo { parents: parent_spaces, number_unread_messages: r.unread_notification_counts().notification_count, notifications, - latest_event: get_events(r, 1, None).await?.events.into_iter().next(), + latest_event: get_events(r, 1, None, None) + .await? + .events + .into_iter() + .next(), }) } } @@ -137,3 +144,37 @@ pub async fn room_avatar( matrix_media_controller::serve_mxc_file(req, uri).await } + +#[derive(serde::Serialize)] +pub struct UserReceipt { + user: OwnedUserId, + event_id: OwnedEventId, + ts: Option, +} + +/// Get room receipts +pub async fn receipts(client: MatrixClientExtractor, path: web::Path) -> HttpResult { + let Some(room) = client.client.client.get_room(&path.room_id) else { + return Ok(HttpResponse::NotFound().json("Room not found")); + }; + + let members = room.members(RoomMemberships::ACTIVE).await?; + + let mut receipts = Vec::new(); + for m in members { + let Some((event_id, receipt)) = room + .load_user_receipt(ReceiptType::Read, ReceiptThread::Main, m.user_id()) + .await? + else { + continue; + }; + + receipts.push(UserReceipt { + user: m.user_id().to_owned(), + event_id, + ts: receipt.ts, + }) + } + + Ok(HttpResponse::Ok().json(receipts)) +} diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 1713864..6137319 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -155,6 +155,10 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/{room_id}/avatar", web::get().to(matrix_room_controller::room_avatar), ) + .route( + "/api/matrix/room/{room_id}/receipts", + web::get().to(matrix_room_controller::receipts), + ) // Matrix profile controller .route( "/api/matrix/profile/{user_id}", @@ -189,7 +193,7 @@ async fn main() -> std::io::Result<()> { "/api/matrix/room/{room_id}/event/{event_id}", web::delete().to(matrix_event_controller::redact_event), ) - .route( + .route( "/api/matrix/room/{room_id}/event/{event_id}/receipt", web::post().to(matrix_event_controller::receipt), ) From 077c64be285bfdd2a97a1c89f7f75956c5978622 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 11:17:02 +0100 Subject: [PATCH 101/124] Forward typing event in WebSocket --- matrixgw_backend/src/broadcast_messages.rs | 3 ++ .../src/controllers/ws_controller.rs | 20 ++++++++++++ .../src/matrix_connection/sync_thread.rs | 31 ++++++++++++++----- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/matrixgw_backend/src/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs index 2e129eb..a398387 100644 --- a/matrixgw_backend/src/broadcast_messages.rs +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -5,6 +5,7 @@ use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent; use matrix_sdk::ruma::events::receipt::SyncReceiptEvent; use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent; use matrix_sdk::ruma::events::room::redaction::OriginalSyncRoomRedactionEvent; +use matrix_sdk::ruma::events::typing::SyncTypingEvent; use matrix_sdk::sync::SyncResponse; pub type BroadcastSender = tokio::sync::broadcast::Sender; @@ -35,6 +36,8 @@ pub enum BroadcastMessage { RoomRedactionEvent(BxRoomEvent), /// Message fully read event ReceiptEvent(BxRoomEvent), + /// User is typing message event + TypingEvent(BxRoomEvent), /// Raw Matrix sync response MatrixSyncResponse { user: UserEmail, sync: SyncResponse }, } diff --git a/matrixgw_backend/src/controllers/ws_controller.rs b/matrixgw_backend/src/controllers/ws_controller.rs index 62553e7..57a80de 100644 --- a/matrixgw_backend/src/controllers/ws_controller.rs +++ b/matrixgw_backend/src/controllers/ws_controller.rs @@ -42,6 +42,12 @@ pub struct WsReceiptEvent { pub receipts: Vec, } +#[derive(Debug, serde::Serialize)] +pub struct WsTypingEvent { + pub room_id: OwnedRoomId, + pub user_ids: Vec, +} + /// Messages sent to the client #[derive(Debug, serde::Serialize)] #[serde(tag = "type")] @@ -57,6 +63,9 @@ pub enum WsMessage { /// Fully read message event ReceiptEvent(WsReceiptEvent), + + /// User is typing event + TypingEvent(WsTypingEvent), } impl WsMessage { @@ -71,6 +80,7 @@ impl WsMessage { data: Box::new(evt.data.content.clone()), })) } + BroadcastMessage::ReactionEvent(evt) if &evt.user == user => { Some(Self::RoomReactionEvent(WsRoomEvent { room_id: evt.room.room_id().to_owned(), @@ -80,6 +90,7 @@ impl WsMessage { data: Box::new(evt.data.content.clone()), })) } + BroadcastMessage::RoomRedactionEvent(evt) if &evt.user == user => { Some(Self::RoomRedactionEvent(WsRoomEvent { room_id: evt.room.room_id().to_owned(), @@ -89,6 +100,7 @@ impl WsMessage { data: Box::new(evt.data.content.clone()), })) } + BroadcastMessage::ReceiptEvent(evt) if &evt.user == user => { let mut receipts = vec![]; for (event_id, r) in &evt.data.content.0 { @@ -108,6 +120,14 @@ impl WsMessage { receipts, })) } + + BroadcastMessage::TypingEvent(evt) if &evt.user == user => { + Some(Self::TypingEvent(WsTypingEvent { + room_id: evt.room.room_id().to_owned(), + user_ids: evt.data.content.user_ids.clone(), + })) + } + _ => None, } } diff --git a/matrixgw_backend/src/matrix_connection/sync_thread.rs b/matrixgw_backend/src/matrix_connection/sync_thread.rs index 439c753..73df0c4 100644 --- a/matrixgw_backend/src/matrix_connection/sync_thread.rs +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -11,6 +11,7 @@ use matrix_sdk::ruma::events::reaction::OriginalSyncReactionEvent; use matrix_sdk::ruma::events::receipt::SyncReceiptEvent; use matrix_sdk::ruma::events::room::message::OriginalSyncRoomMessageEvent; use matrix_sdk::ruma::events::room::redaction::OriginalSyncRoomRedactionEvent; +use matrix_sdk::ruma::events::typing::SyncTypingEvent; use ractor::ActorRef; #[derive(Clone, Debug, Eq, PartialEq)] @@ -54,11 +55,11 @@ async fn sync_thread_task( let mut handlers = vec![]; let tx_msg_handle = tx.clone(); - let user_msg_handle = client.email.clone(); + let user_msg_mail = client.email.clone(); handlers.push(client.add_event_handler( async move |event: OriginalSyncRoomMessageEvent, room: Room| { if let Err(e) = tx_msg_handle.send(BroadcastMessage::RoomMessageEvent(BxRoomEvent { - user: user_msg_handle.clone(), + user: user_msg_mail.clone(), data: Box::new(event), room, })) { @@ -68,11 +69,11 @@ async fn sync_thread_task( )); let tx_reac_handle = tx.clone(); - let user_reac_handle = client.email.clone(); + let user_reac_mail = client.email.clone(); handlers.push(client.add_event_handler( async move |event: OriginalSyncReactionEvent, room: Room| { if let Err(e) = tx_reac_handle.send(BroadcastMessage::ReactionEvent(BxRoomEvent { - user: user_reac_handle.clone(), + user: user_reac_mail.clone(), data: Box::new(event), room, })) { @@ -82,12 +83,12 @@ async fn sync_thread_task( )); let tx_redac_handle = tx.clone(); - let user_redac_handle = client.email.clone(); + let user_redac_mail = client.email.clone(); handlers.push(client.add_event_handler( async move |event: OriginalSyncRoomRedactionEvent, room: Room| { if let Err(e) = tx_redac_handle.send(BroadcastMessage::RoomRedactionEvent(BxRoomEvent { - user: user_redac_handle.clone(), + user: user_redac_mail.clone(), data: Box::new(event), room, })) @@ -98,11 +99,11 @@ async fn sync_thread_task( )); let tx_receipt_handle = tx.clone(); - let user_receipt_handle = client.email.clone(); + let user_receipt_mail = client.email.clone(); handlers.push( client.add_event_handler(async move |event: SyncReceiptEvent, room: Room| { if let Err(e) = tx_receipt_handle.send(BroadcastMessage::ReceiptEvent(BxRoomEvent { - user: user_receipt_handle.clone(), + user: user_receipt_mail.clone(), data: Box::new(event), room, })) { @@ -111,6 +112,20 @@ async fn sync_thread_task( }), ); + let tx_typing_handle = tx.clone(); + let user_typing_mail = client.email.clone(); + handlers.push( + client.add_event_handler(async move |event: SyncTypingEvent, room: Room| { + if let Err(e) = tx_typing_handle.send(BroadcastMessage::TypingEvent(BxRoomEvent { + user: user_typing_mail.clone(), + data: Box::new(event), + room, + })) { + log::warn!("Failed to forward typing event! {e}"); + } + }), + ); + loop { tokio::select! { // Message from tokio broadcast From 32354f79eae69350e4988e29ed7648f48e964dcf Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 11:18:58 +0100 Subject: [PATCH 102/124] Add typing event definition --- matrixgw_frontend/src/api/WsApi.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/matrixgw_frontend/src/api/WsApi.ts b/matrixgw_frontend/src/api/WsApi.ts index 6104170..556851e 100644 --- a/matrixgw_frontend/src/api/WsApi.ts +++ b/matrixgw_frontend/src/api/WsApi.ts @@ -58,6 +58,13 @@ export interface RoomReceiptEvent { receipts: ReceiptEventEntry[]; } +export interface RoomTypingEvent { + time: number; + type: "TypingEvent"; + room_id: string; + user_ids: string[]; +} + export type WsMessage = | RoomMessageEvent | RoomReactionEvent From 30e63bfdb444b8afe190d7d03c150bdab48c9545 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 17:23:15 +0100 Subject: [PATCH 103/124] Handle typing events --- matrixgw_frontend/src/api/WsApi.ts | 3 +- .../src/utils/RoomEventsManager.ts | 5 +++ .../src/widgets/messages/RoomWidget.tsx | 6 ++-- .../src/widgets/messages/TypingNotice.tsx | 35 +++++++++++++++++++ 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 matrixgw_frontend/src/widgets/messages/TypingNotice.tsx diff --git a/matrixgw_frontend/src/api/WsApi.ts b/matrixgw_frontend/src/api/WsApi.ts index 556851e..334ca21 100644 --- a/matrixgw_frontend/src/api/WsApi.ts +++ b/matrixgw_frontend/src/api/WsApi.ts @@ -69,7 +69,8 @@ export type WsMessage = | RoomMessageEvent | RoomReactionEvent | RoomRedactionEvent - | RoomReceiptEvent; + | RoomReceiptEvent + | RoomTypingEvent; export class WsApi { /** diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 5a4d041..08a5a95 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -31,11 +31,13 @@ export class RoomEventsManager { private events: MatrixEvent[]; messages: Message[]; endToken?: string; + typingUsers: string[]; constructor(room: Room, initialMessages: MatrixEventsList) { this.room = room; this.events = []; this.messages = []; + this.typingUsers = []; this.processNewEvents(initialMessages); } @@ -88,6 +90,9 @@ export class RoomEventsManager { file: m.data.file, }, }; + } else if (m.type === "TypingEvent") { + this.typingUsers = m.user_ids; + return true; } else { // Ignore event console.info("Event not supported => ignored"); diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx index 61d33be..5dce7ea 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -1,11 +1,12 @@ import React from "react"; +import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import type { Room } from "../../api/matrix/MatrixApiRoom"; +import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider"; import { RoomEventsManager } from "../../utils/RoomEventsManager"; import { RoomMessagesList } from "./RoomMessagesList"; import { SendMessageForm } from "./SendMessageForm"; -import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; -import { useSnackbar } from "../../hooks/contexts_provider/SnackbarProvider"; +import { TypingNotice } from "./TypingNotice"; export function RoomWidget(p: { room: Room; @@ -36,6 +37,7 @@ export function RoomWidget(p: { onClick={handleRoomClick} > +
    ); diff --git a/matrixgw_frontend/src/widgets/messages/TypingNotice.tsx b/matrixgw_frontend/src/widgets/messages/TypingNotice.tsx new file mode 100644 index 0000000..4223811 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/TypingNotice.tsx @@ -0,0 +1,35 @@ +import { Typography } from "@mui/material"; +import React from "react"; +import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; +import type { RoomEventsManager } from "../../utils/RoomEventsManager"; +import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; + +export function TypingNotice(p: { + users: UsersMap; + manager: RoomEventsManager; +}): React.ReactElement { + const user = useUserInfo(); + + const users = React.useMemo( + () => + [...p.users.values()].filter( + (u) => + p.manager.typingUsers.includes(u.user_id) && + u.user_id !== user.info.matrix_user_id + ), + [p.manager.typingUsers] + ); + + if (users.length === 0) return <>; + + return ( + + {users.map((u) => u.display_name ?? u.display_name).join(", ")}{" "} + {users.length > 1 ? "are" : "is"} typing... + + ); +} From 7356a66e4a9fc8dca7fa27114bfd18cda8b57704 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 18:30:22 +0100 Subject: [PATCH 104/124] Handle read receipts on web ui --- .../src/api/matrix/MatrixApiRoom.ts | 18 +++++ matrixgw_frontend/src/utils/DateUtils.ts | 9 +++ .../src/utils/RoomEventsManager.ts | 68 ++++++++++++++++++- .../src/widgets/messages/AccountIcon.tsx | 6 +- .../widgets/messages/MainMessagesWidget.tsx | 3 +- .../src/widgets/messages/RoomMessagesList.tsx | 21 +++++- 6 files changed, 117 insertions(+), 8 deletions(-) diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts index 8cfbcbc..9809143 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts @@ -15,6 +15,12 @@ export interface Room { latest_event?: MatrixEvent; } +export interface Receipt { + user: string; + event_id: string; + ts: number; +} + /** * Find main member of room */ @@ -53,4 +59,16 @@ export class MatrixApiRoom { }) ).data; } + + /** + * Get a room receipts + */ + static async RoomReceipts(room: Room): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: `/matrix/room/${room.id}/receipts`, + }) + ).data; + } } diff --git a/matrixgw_frontend/src/utils/DateUtils.ts b/matrixgw_frontend/src/utils/DateUtils.ts index c44c16f..7c680b9 100644 --- a/matrixgw_frontend/src/utils/DateUtils.ts +++ b/matrixgw_frontend/src/utils/DateUtils.ts @@ -6,3 +6,12 @@ export function time(): number { return Math.floor(new Date().getTime() / 1000); } + +/** + * Get UNIX time + * + * @returns Number of milliseconds since Epoch + */ +export function timeMs(): number { + return new Date().getTime(); +} diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 08a5a95..d324ec5 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -5,8 +5,9 @@ import type { MatrixEventsList, MessageType, } from "../api/matrix/MatrixApiEvent"; -import type { Room } from "../api/matrix/MatrixApiRoom"; +import type { Receipt, Room } from "../api/matrix/MatrixApiRoom"; import type { WsMessage } from "../api/WsApi"; +import { timeMs } from "./DateUtils"; export interface MessageReaction { event_id: string; @@ -29,15 +30,23 @@ export interface Message { export class RoomEventsManager { readonly room: Room; private events: MatrixEvent[]; + private receipts: Receipt[]; messages: Message[]; endToken?: string; typingUsers: string[]; + receiptsEventsMap: Map; - constructor(room: Room, initialMessages: MatrixEventsList) { + constructor( + room: Room, + initialMessages: MatrixEventsList, + receipts: Receipt[] + ) { this.room = room; this.events = []; + this.receipts = receipts; this.messages = []; this.typingUsers = []; + this.receiptsEventsMap = new Map(); this.processNewEvents(initialMessages); } @@ -90,9 +99,30 @@ export class RoomEventsManager { file: m.data.file, }, }; + } else if (m.type === "ReceiptEvent") { + for (const r of m.receipts) { + const prevReceipt = this.receipts.find( + (needle) => r.user === needle.user + ); + // Create new receipt + if (!prevReceipt) + this.receipts.push({ + user: r.user, + event_id: r.event, + ts: r.ts ?? timeMs(), + }); + // Update receipt + else { + prevReceipt.event_id = r.event; + prevReceipt.ts = r.ts ?? timeMs(); + } + } + + this.rebuildMessagesList(); + return true; // Emphemeral event } else if (m.type === "TypingEvent") { this.typingUsers = m.user_ids; - return true; + return true; // Not a real event } else { // Ignore event console.info("Event not supported => ignored"); @@ -117,6 +147,12 @@ export class RoomEventsManager { // Sorts events list to process oldest events first this.events.sort((a, b) => a.time - b.time); + // Process receipts (users map) + const receiptsUsersMap = new Map(); + for (const r of this.receipts) { + receiptsUsersMap.set(r.user, { ...r }); + } + // First, process redactions to skip redacted events let redacted = new Set( this.events @@ -144,6 +180,24 @@ export class RoomEventsManager { continue; } + // Else it is a new message; update receipts if needed + else { + let userReceipt = receiptsUsersMap.get(evt.sender); + + // Create fake receipt if none is available + if (!userReceipt) + receiptsUsersMap.set(evt.sender, { + event_id: evt.id, + ts: evt.time, + user: evt.sender, + }); + // If the message is more recent than user receipt, replace the receipt + else if (userReceipt.ts < evt.time) { + userReceipt.event_id = evt.id; + userReceipt.ts = evt.time; + } + } + this.messages.push({ event_id: evt.id, account: evt.sender, @@ -175,5 +229,13 @@ export class RoomEventsManager { }); } } + + // Adapt receipts to be event-indexed + this.receiptsEventsMap.clear(); + for (const [_userId, receipt] of receiptsUsersMap) { + if (!this.receiptsEventsMap.has(receipt.event_id)) + this.receiptsEventsMap.set(receipt.event_id, [receipt]); + else this.receiptsEventsMap.get(receipt.event_id)!.push(receipt); + } } } diff --git a/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx b/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx index 3c6e23a..e4fb748 100644 --- a/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx +++ b/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx @@ -2,12 +2,16 @@ import { Avatar } from "@mui/material"; import type { UserProfile } from "../../api/matrix/MatrixApiProfile"; import { MatrixApiMedia } from "../../api/matrix/MatrixApiMedia"; -export function AccountIcon(p: { user: UserProfile }): React.ReactElement { +export function AccountIcon(p: { + user: UserProfile; + size?: number; +}): React.ReactElement { return ( {p.user.display_name?.slice(0, 1)} diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index 05bc37f..d5c60a8 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -89,7 +89,8 @@ function _MainMessageWidget(p: { return; } const messages = await MatrixApiEvent.GetRoomEvents(currentRoom); - const mgr = new RoomEventsManager(currentRoom!, messages); + const receipts = await MatrixApiRoom.RoomReceipts(currentRoom); + const mgr = new RoomEventsManager(currentRoom!, messages, receipts); setRoomMgr(mgr); }; diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 480adc9..ca50878 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -21,7 +21,7 @@ import EmojiPicker, { EmojiStyle, Theme } from "emoji-picker-react"; import React from "react"; import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; -import type { Room } from "../../api/matrix/MatrixApiRoom"; +import type { Receipt, Room } from "../../api/matrix/MatrixApiRoom"; import { useAlert } from "../../hooks/contexts_provider/AlertDialogProvider"; import { useConfirm } from "../../hooks/contexts_provider/ConfirmDialogProvider"; import { useLoadingMessage } from "../../hooks/contexts_provider/LoadingMessageProvider"; @@ -73,6 +73,7 @@ export function RoomMessagesList(p: { m.time_sent_dayjs.startOf("day").unix() != p.manager.messages[idx - 1].time_sent_dayjs.startOf("day").unix() } + receipts={p.manager.receiptsEventsMap.get(m.event_id)} /> ))} @@ -87,6 +88,7 @@ function RoomMessage(p: { message: Message; previousFromSamePerson: boolean; firstMessageOfDay: boolean; + receipts?: Receipt[]; }): React.ReactElement { const theme = useTheme(); const user = useUserInfo(); @@ -220,7 +222,7 @@ function RoomMessage(p: { {/** Message itself */} -
    +
    {/* Image */} {p.message.type === "m.image" && ( {p.message.content}
    )}
    + + {/* Read receipts */} +
    + {(p.receipts ?? []).map((r) => { + const u = p.users.get(r.user); + + if (!u || u.user_id === user.info.matrix_user_id) return <>; + + return ; + })} +
    + + {/** Button bar */} - {/* Reaction */} + {/* Reactions */} {[...p.message.reactions.keys()].map((r) => { const reactions = p.message.reactions.get(r)!; From 6d78930b895abe850b8e9eb21a132f117e8a5b2f Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 19:05:37 +0100 Subject: [PATCH 105/124] Add link on unlinked account message --- .../src/widgets/NotLinkedAccountMessage.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/matrixgw_frontend/src/widgets/NotLinkedAccountMessage.tsx b/matrixgw_frontend/src/widgets/NotLinkedAccountMessage.tsx index 65412b3..500476f 100644 --- a/matrixgw_frontend/src/widgets/NotLinkedAccountMessage.tsx +++ b/matrixgw_frontend/src/widgets/NotLinkedAccountMessage.tsx @@ -1,3 +1,7 @@ +import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; +import { Button } from "@mui/material"; +import { Link } from "react-router"; + export function NotLinkedAccountMessage(): React.ReactElement { return (
    - Your Matrix account is not linked yet! +
    + Your Matrix account is not linked yet! +
    + + +
    ); } From 3274d076352dedcf1ade820ad33a5772f6d63b81 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 19:06:58 +0100 Subject: [PATCH 106/124] Do not display spaces bar if it is useless --- matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx b/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx index 8720325..1557b16 100644 --- a/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx +++ b/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx @@ -16,6 +16,9 @@ export function SpaceSelector(p: { [p.rooms] ); + // Do not display space bar if your is not member of any space + if (spaces.length === 0) return <>; + return (
    Date: Mon, 1 Dec 2025 19:29:18 +0100 Subject: [PATCH 107/124] Switch to native emojies --- matrixgw_frontend/src/widgets/EmojiIcon.tsx | 2 +- .../src/widgets/messages/RoomMessagesList.tsx | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/matrixgw_frontend/src/widgets/EmojiIcon.tsx b/matrixgw_frontend/src/widgets/EmojiIcon.tsx index a51b780..cde45a0 100644 --- a/matrixgw_frontend/src/widgets/EmojiIcon.tsx +++ b/matrixgw_frontend/src/widgets/EmojiIcon.tsx @@ -19,6 +19,6 @@ function emojiUnicode(emoji: string): string { export function EmojiIcon(p: { emojiKey: string }): React.ReactElement { const unified = emojiUnicode(p.emojiKey); return ( - + ); } diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index ca50878..295c57b 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -379,7 +379,7 @@ function RoomMessage(p: { flexDirection: "row", }} > -
    +
    @@ -407,7 +407,7 @@ function RoomMessage(p: { {/* Pick reaction dialog */} handleSelectEmoji(emoji.emoji)} /> @@ -475,7 +475,14 @@ function ReactionButton(p: { return <>; return ( - ); From 118b73fce92a2233945f9e9542bbc151a2aa8e2e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 1 Dec 2025 19:32:50 +0100 Subject: [PATCH 108/124] Improve receipts spacing --- .../src/widgets/messages/RoomMessagesList.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 295c57b..80274de 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -288,13 +288,23 @@ function RoomMessage(p: {
    {/* Read receipts */} -
    +
    {(p.receipts ?? []).map((r) => { const u = p.users.get(r.user); if (!u || u.user_id === user.info.matrix_user_id) return <>; - return ; + return ( +
    + +
    + ); })}
    From ab136ef6d0e59aabd77f0603f06ac18dd405b88e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 2 Dec 2025 13:59:14 +0100 Subject: [PATCH 109/124] Display a message when the conversation is empty --- .../src/widgets/messages/RoomMessagesList.tsx | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 80274de..f0c0aff 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -40,16 +40,34 @@ export function RoomMessagesList(p: { users: UsersMap; manager: RoomEventsManager; }): React.ReactElement { + const listContainerRef = React.createRef(); const messagesEndRef = React.createRef(); // Automatically scroll to bottom when number of messages change React.useEffect(() => { if (messagesEndRef) messagesEndRef.current?.scrollIntoView({ behavior: "instant" }); - }, [p.manager.messages.length]); + }, [p.manager.messages.at(-1)?.event_id]); + + // Watch scroll to detect when user reach the top to load older messages + const handleScroll = () => { + if (!listContainerRef.current) return; + + const { scrollTop } = listContainerRef.current; + + if (scrollTop !== 0) { + return; + } + + console.log("reached top"); + // TODO : load old messages + // TODO : block when user reached begining of conversation + }; return (
    + {/* Empty conversation notice */} + {p.manager.messages.length === 0 && ( +
    + No message in this conversation yet! +
    + )} + {p.manager.messages.map((m, idx) => ( Date: Wed, 3 Dec 2025 08:53:44 +0100 Subject: [PATCH 110/124] Can filter to show only unread rooms --- .../src/widgets/messages/RoomSelector.tsx | 103 +++++++++++------- 1 file changed, 64 insertions(+), 39 deletions(-) diff --git a/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx b/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx index c908696..258402e 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx @@ -6,6 +6,7 @@ import { ListItemIcon, ListItemText, } from "@mui/material"; +import React from "react"; import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; import { roomName, type Room } from "../../api/matrix/MatrixApiRoom"; import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; @@ -21,6 +22,13 @@ export function RoomSelector(p: { }): React.ReactElement { const user = useUserInfo(); + const [unread, setUnread] = React.useState(false); + + const shownRooms = React.useMemo( + () => p.rooms.filter((r) => !unread || r.number_unread_messages > 0), + [p.rooms, unread] + ); + if (p.rooms.length === 0) return (
    - {p.rooms.map((r) => ( - - ) - } - disablePadding - > - p.onChange(r)} - dense - selected={p.currRoom?.id === r.id} +
    + {/** Chip bar */} +
    + setUnread(!unread)} style={{ cursor: "pointer" }}> + + +
    + + {/** Rooms list */} + + {shownRooms.map((r) => ( + + ) + } + disablePadding > - - - - 0 ? "bold" : undefined, - }} - > - {roomName(user.info, r, p.users)} - - } - /> - - - ))} - + p.onChange(r)} + dense + selected={p.currRoom?.id === r.id} + > + + + + 0 ? "bold" : undefined, + }} + > + {roomName(user.info, r, p.users)} + + } + /> + + + ))} + +
    ); } From 4110f4d063d616b021b2b70ccc86b1fae8ed8803 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 09:11:02 +0100 Subject: [PATCH 111/124] Can load older messages in conversations --- .../src/utils/RoomEventsManager.ts | 4 ++ .../src/widgets/messages/RoomMessagesList.tsx | 52 +++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index d324ec5..4ac527d 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -36,6 +36,10 @@ export class RoomEventsManager { typingUsers: string[]; receiptsEventsMap: Map; + get canLoadOlder(): boolean { + return !!this.endToken; + } + constructor( room: Room, initialMessages: MatrixEventsList, diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index f0c0aff..4553ab6 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -7,6 +7,7 @@ import { Button, ButtonGroup, Chip, + CircularProgress, Dialog, DialogActions, DialogContent, @@ -40,6 +41,10 @@ export function RoomMessagesList(p: { users: UsersMap; manager: RoomEventsManager; }): React.ReactElement { + const snackbar = useSnackbar(); + + const [loadingOlder, setLoadingOlder] = React.useState(false); + const listContainerRef = React.createRef(); const messagesEndRef = React.createRef(); @@ -50,8 +55,9 @@ export function RoomMessagesList(p: { }, [p.manager.messages.at(-1)?.event_id]); // Watch scroll to detect when user reach the top to load older messages - const handleScroll = () => { - if (!listContainerRef.current) return; + const handleScroll = async () => { + if (!listContainerRef.current || loadingOlder || !p.manager.canLoadOlder) + return; const { scrollTop } = listContainerRef.current; @@ -59,9 +65,20 @@ export function RoomMessagesList(p: { return; } - console.log("reached top"); - // TODO : load old messages - // TODO : block when user reached begining of conversation + setLoadingOlder(true); + + try { + const older = await MatrixApiEvent.GetRoomEvents( + p.room, + p.manager.endToken + ); + p.manager.processNewEvents(older); + } catch (e) { + console.error("Failed to load older messages!", e); + snackbar(`Failed to load older messages for conversation! ${e}`); + } finally { + setLoadingOlder(false); + } }; return ( @@ -90,6 +107,31 @@ export function RoomMessagesList(p: {
    )} + {/** Begining of conversation */} + {!p.manager.canLoadOlder && ( + + Begining of conversation + + )} + + {/** Loading older messages spinner */} + {loadingOlder && ( +
    + +
    + )} + {p.manager.messages.map((m, idx) => ( Date: Wed, 3 Dec 2025 09:43:04 +0100 Subject: [PATCH 112/124] Add production Makefile --- Makefile | 18 ++++ matrixgw_backend/.gitignore | 1 + matrixgw_backend/Cargo.lock | 80 ++++++++++++++++++ matrixgw_backend/Cargo.toml | 4 +- .../docker/matrixgw_backend/Dockerfile | 10 +++ matrixgw_backend/src/controllers/mod.rs | 1 + .../src/controllers/static_controller.rs | 45 ++++++++++ matrixgw_backend/src/main.rs | 8 +- matrixgw_frontend/public/favicon.png | Bin 0 -> 4710 bytes matrixgw_frontend/public/vite.svg | 1 - 10 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 Makefile create mode 100644 matrixgw_backend/docker/matrixgw_backend/Dockerfile create mode 100644 matrixgw_backend/src/controllers/static_controller.rs create mode 100644 matrixgw_frontend/public/favicon.png delete mode 100644 matrixgw_frontend/public/vite.svg diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ad8433a --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +DOCKER_TEMP_DIR=temp + +all: frontend backend + +frontend: + cd matrixgw_frontend && npm run build && cd .. + rm -rf matrixgw_backend/static + mv matrixgw_frontend/dist matrixgw_backend/static + +backend: frontend + cd matrixgw_backend && cargo clippy -- -D warnings && cargo build --release + +backend_docker: backend + rm -rf $(DOCKER_TEMP_DIR) + mkdir $(DOCKER_TEMP_DIR) + cp matrixgw_backend/target/release/matrixgw_backend $(DOCKER_TEMP_DIR) + docker build -t pierre42100/matrix_gateway -f matrixgw_backend/docker/matrixgw_backend/Dockerfile "$(DOCKER_TEMP_DIR)" + rm -rf $(DOCKER_TEMP_DIR) diff --git a/matrixgw_backend/.gitignore b/matrixgw_backend/.gitignore index f3a3b05..afb9e89 100644 --- a/matrixgw_backend/.gitignore +++ b/matrixgw_backend/.gitignore @@ -2,3 +2,4 @@ storage app_storage .idea target +static \ No newline at end of file diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index f44d616..8666560 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -3077,8 +3077,10 @@ dependencies = [ "log", "mailchecker", "matrix-sdk", + "mime_guess", "ractor", "rand 0.9.2", + "rust-embed", "serde", "serde_json", "sha2", @@ -3124,6 +3126,16 @@ version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbf6f36070878c42c5233846cd3de24cf9016828fd47bc22957a687298bb21fc" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4196,6 +4208,40 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-stemmers" version = "1.2.0" @@ -4279,6 +4325,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -5278,6 +5333,12 @@ dependencies = [ "web-time", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -5417,6 +5478,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -5563,6 +5634,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index 84f0947..454ea41 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -33,4 +33,6 @@ ractor = "0.15.9" serde_json = "1.0.145" lazy-regex = "3.4.2" actix-ws = "0.3.0" -infer = "0.19.0" \ No newline at end of file +infer = "0.19.0" +rust-embed = "8.9.0" +mime_guess = "2.0.5" \ No newline at end of file diff --git a/matrixgw_backend/docker/matrixgw_backend/Dockerfile b/matrixgw_backend/docker/matrixgw_backend/Dockerfile new file mode 100644 index 0000000..0ff5868 --- /dev/null +++ b/matrixgw_backend/docker/matrixgw_backend/Dockerfile @@ -0,0 +1,10 @@ +FROM debian:bookworm-slim + +RUN apt-get update \ + && apt-get install -y libcurl4 libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + + +COPY matrixgw_backend /usr/local/bin/matrixgw_backend + +ENTRYPOINT ["/usr/local/bin/matrixgw_backend"] \ No newline at end of file diff --git a/matrixgw_backend/src/controllers/mod.rs b/matrixgw_backend/src/controllers/mod.rs index 75ceeed..9dfcf05 100644 --- a/matrixgw_backend/src/controllers/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -7,6 +7,7 @@ pub mod matrix; pub mod matrix_link_controller; pub mod matrix_sync_thread_controller; pub mod server_controller; +pub mod static_controller; pub mod tokens_controller; pub mod ws_controller; diff --git a/matrixgw_backend/src/controllers/static_controller.rs b/matrixgw_backend/src/controllers/static_controller.rs new file mode 100644 index 0000000..93f8073 --- /dev/null +++ b/matrixgw_backend/src/controllers/static_controller.rs @@ -0,0 +1,45 @@ +#[cfg(debug_assertions)] +pub use serve_static_debug::{root_index, serve_static_content}; +#[cfg(not(debug_assertions))] +pub use serve_static_release::{root_index, serve_static_content}; + +#[cfg(debug_assertions)] +mod serve_static_debug { + use actix_web::{HttpResponse, Responder}; + + pub async fn root_index() -> impl Responder { + HttpResponse::Ok().body("Hello world! Debug=on for Matrix Gateway!") + } + + pub async fn serve_static_content() -> impl Responder { + HttpResponse::NotFound().body("Hello world! Static assets are not served in debug mode") + } +} + +#[cfg(not(debug_assertions))] +mod serve_static_release { + use actix_web::{HttpResponse, Responder, web}; + use rust_embed::RustEmbed; + + #[derive(RustEmbed)] + #[folder = "static/"] + struct Asset; + + fn handle_embedded_file(path: &str, can_fallback: bool) -> HttpResponse { + match (Asset::get(path), can_fallback) { + (Some(content), _) => HttpResponse::Ok() + .content_type(mime_guess::from_path(path).first_or_octet_stream().as_ref()) + .body(content.data.into_owned()), + (None, false) => HttpResponse::NotFound().body("404 Not Found"), + (None, true) => handle_embedded_file("index.html", false), + } + } + + pub async fn root_index() -> impl Responder { + handle_embedded_file("index.html", false) + } + + pub async fn serve_static_content(path: web::Path) -> impl Responder { + handle_embedded_file(path.as_ref(), !path.as_ref().starts_with("static/")) + } +} diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs index 6137319..d2bcccf 100644 --- a/matrixgw_backend/src/main.rs +++ b/matrixgw_backend/src/main.rs @@ -15,7 +15,7 @@ use matrixgw_backend::controllers::matrix::{ }; use matrixgw_backend::controllers::{ auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller, - tokens_controller, ws_controller, + static_controller, tokens_controller, ws_controller, }; use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor; use matrixgw_backend::users::User; @@ -202,6 +202,12 @@ async fn main() -> std::io::Result<()> { "/api/matrix/media/{mxc}", web::get().to(matrix_media_controller::serve_mxc_handler), ) + // Static assets + .route("/", web::get().to(static_controller::root_index)) + .route( + "/{tail:.*}", + web::get().to(static_controller::serve_static_content), + ) }) .workers(4) .bind(&AppConfig::get().listen_address)? diff --git a/matrixgw_frontend/public/favicon.png b/matrixgw_frontend/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..6027e50857e6f7716c0c234de212d40a22a768b0 GIT binary patch literal 4710 zcmd^D`9D-`+`i9oMl*zA$j)Hw5hW?gSbC5p6-kMavTu*<6=!5AZ3<;?TB&T=qnJ>L zkR?lxjHP6kJW7^%&-A|U=l$jV1K!W)onOw}bFS}oUHA9g_nG_qy=-e^#>XSU0|4N& zFgHC60EB&o0G=DWI0fao10bYfVQOR-?mV0A)9h|9(lmi9xO^)yKeq90+pQUs7`u<2 z0gOq;z1#Qm9=WHd-1NFnyD5M9$S8Homq)x0338shAGz92QU}jSEt=qNNf>v3ag(LQ zR} zz*Ige!wtq$n_4`+5vd>#!UtOc=$qckW@#7?b^`i!0vSjv-@Ru7AO<(NQyW4UW8*u5 z@np~g{(p-KCfAjfE!l*G#SR*I2iWN!^BcM(*V(IwY&`I-o$zk4;q#`|lz$eZ{*hkSM*c3xeowFP}wO z6;XjFuY7DAX87q7ajdgK^!clCFqC$&OsOurx6Z<^LCU{h1Xd^X7vUVtkvR!2URW!s zM2YR=k<2D>oh+6_jw;X$1)Vhoe_8vR;+fQXA=I*Lx^mN8yFamCl}g=$K8k;C{ki?YFW)`8~xG)*DCM&DdN*VQ-{h6TEKQ&hn%7)+*0dz>!ES=T$`DqCRJI%-*eQb0iybO;9ZIorUaQDl3OK*D zNlOq#o^EB6Xs!|#Eb-m?Hu4KU&2zu!z|2cTpd;F-ow|Jaq>WoZlta5A)NY>XF;0imt9ReBJT#tTzat{>V8?SY`dkNuXZD%0-<=XaR>}>j;Ol-C3Zjn`YrID4W8zRC96Nlj z4^PcWCaZ?Lv0w@x?~fN^QCMAFyY?@Y-m~k@dJdb9#f6YDpO%&`!W~7Mwp)k1;UTAr zExRJepqcRC<#kunZfOkW@m9tNsdjW9CKhx0{zXP=FU&J@^kVOY1*1l$-cq-q3%XL90Psv^! z-Ve4@3tAOnTZh_H5B7so5l6rAL3vtUu5t>%RmCSH7W8DixKt&G;uGyxso<&2aMo)D za2C#w4nr6lQv9+Itd(_A;ha3UMhWWFBBDn9#|H_Jr)uUCHzRQP@=lK%K<7?ZEMx(; zoRNuA^{!nUL{Yo!d;8)Mt`=HD3t#n>^W`*` z$OBC)vF{>`_Yzbv*yeNn&~v!isJ!?f!U%-brevTz701uEf?%8KOf?Dc{h~fv4=-2f zbq3?wnfry(vtq_?^6yr)LnOAAI3as?n-a_wi_36@fqi~F&lpgwL9Z= z>flb`qMi9``M!Yu)ImQJ@c8!4>!XuKK=a7HUDkTJ?_Y_XuFog zT~SWpS4YT7Z+Ah$$0Z)>#dD?vC&TX)-Ab4kcJA7s1!-bm>O#dg%0lv^7_U$qgFe8! zZ}ywvzXkiDW5P=dn{HX{y~#Dk+Yx<0N`n^_VTg?ORQ_3W{H{}%rNmsqH;#;YE~=y4 zXX>WeC;a?)t{PX2n4w*L(D+oHZB$U0WDYw8&2)l1ZRbT|haaOVo{~{Wa|mJ+uEg>h zjR-J$yfZ`^!@RDnhV>kYy15*LGr&_{`#uii+#nto0s36)RGv#Q6(cxoC$_mq_KmC@ zB;2!y!Yj!Jx8eH?ziUV3k81OL`5a10#O z$g~)Ux_3k1(ae!%-8DRuhheLf5W+F3mEi=nAt*2Xu-a?aRvsp#Iut8{COb~?uy!t< z%gh)O(E6gmiJ-lk{H#&2u$FtZc0h#bD-sy9FTh@32luY|kH4!~uUD!$3DdLYWm+Mc zA0{-sS0AC`A9T+X(xwj>)WWBqBx2E~1LLV^4ctjKydPb4VSbXm-s1u!tjjg$$W4WP9IV^-GL9kk#TYx zO#r9y;(~+aw8l-yiVCiGh8HGE9cieo|8ckesXk;*Kl03PvIvw1O>58j*TPFFP6#8{ zw`12)gtjj#`5#Fr?euXaabZ}irL0X9yU5n%EdK>0+H~Pg+Tk611gIIQcz6O~P}Zw| z{{^Pr-BCY|Fyjacf>a=}l?!3Ke&{YC1Sb)EEh_O;i{BytUIRU;+Z@X9C@<-+^El5^ zZX&AtBg4%u9|YNI%#a|JEHts&45%Hry~_?^TJq>)9M~?Bu=0K^CkN7*w}Y>Ml!q=3 z!+7e+m7x7sK$VtjV-fKCh#f55ozGrP6ru7zjk_QZ;z+iKe&VV7TNdLJLAZr$0?;|e z^}p=v8UoZw-V(QNS3cG|V^KG)cEIG9+dU71`r-EJ3)+{SJ8WK-zg^SYo~I%jD`7$_ z_5RYJUFf`6@~H;0Kdn4fHybA_du`6wGHe{3T0Nv*huxs%kv^|cRQoXhLn9t?+`(b-~m zx~O}?&wII~<7mZ{19;c!df;uj_eA+E;kVjp%!Z6LS+SQtokMk?V?FGIyF=}QzA+t7 z9e~sdY5P#Zq_hyw*OskK2j?xF4M}q^@*BJ{j;0Y~50BLjjVno?l}%|E5!P#|?`gn* z(zJXc+VVsy$AU&Gt^Zg~YHouf=!lDO8fivY;_K^4QtiCI^ZjuQgO`4x%)8xBl}*2xWia??aeTE@iy*cF92BvAOQRqRw&lC$R=ysJN!h3LO8f zN`%B7M1E7qU}gtZStLjMl#TVSy$amMblhHbau$8Qr~MxRI7wV<&VM731s@rMi=|4yQkLlj!Y7DHRfW#pb5k*m+J{>buZER$Y)+-@noJr05eb zS9NaG5YX8|(nZ~s*?8d-eEF7VcN2UNnvfUyk~ls0UoX+6wKk>b!s*`$ou40o{9uZ< zo~IJ*lMrm{dAAO~xZGU3!w?h<@jwh>Qng$m+M@p1O9OM92zx6#;Jf%KY-3Ya19LO1kG&DHaN0(HAPECg97u?j6-Zla zf9YBg2IJ9EVzblKrB-?I32N zL~Sd;QsK+)$R0Vx4T(b7i^pq)`$_ld$f9n8uB$Dw79^Mdls#k)q73y%nlQ77s346;PnJa!rLp9D8@KK@1=4$N{`mAaDQzZKpWF z*A*~Z!|A+jqny$0bR0{dv~+#zYW50qYdnBi<7tOD^OB-%;rQJCp7Muo*xf@c&4`?W z0UFbdFo4Z++82)A05?ayMY{AQmK5b-D)eh?CRVmJO8H#dA}4FU7dc+XuMOyN?oR+t z3LV{aWv~giMA|^bEdd^@^I@S0;>cfEm93}UAqT9Yl-=6iDr2K`0b#VDloTbklAWb3 zd57a}FO?%+k(T;LT$zU>E>c>;A!j2TI_0&fso-nkNFqo5TQXmEb%w87a=F zq|ujC!Fy;f%Yhpm?aQSz{{vW-=ct>sJvPJU{`IuTe-VxbUld1_I48)*ZUTQe8hooc z9R8t#*vd+lbnnI*2YNqBjX}kGn_tgo!b<)dKmk{_m;3zQ^-Z6>Wx_wrh}Ol4i=+Uv z)?kg~VZ!E8HS9UqKfFwqOTsz{NeSxCa<(|EKai*+NI@2{wYeDhMjd%dMuFW;rI=W% z4Ay6mDGZOJieSwNwJy^nVl&oNXl@WGBId;a*mi#gUI>oBI3ub?MPPf3;Hf#u$e1hu zA!GrEm&Obpj6lRo-W)%UC477&(E)E~1fV>5YlJ{Iz&H(@zVp!kkD6MOg?37N`#%m_ Q1=z=8zl~|3F)ijl0KG@Y`v3p{ literal 0 HcmV?d00001 diff --git a/matrixgw_frontend/public/vite.svg b/matrixgw_frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/matrixgw_frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From e215fe6484511ee64fe67b64cce5898cab73544e Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 09:45:46 +0100 Subject: [PATCH 113/124] Add Renovate config --- .drone.yml | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .drone.yml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..e81ad07 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,103 @@ +--- +kind: pipeline +type: docker +name: default + +steps: +# Frontend +- name: web_build + image: node:23 + volumes: + - name: web_app + path: /tmp/web_build + commands: + - cd matrixgw_backend + - npm install + - npm run lint + - npm run build + - mv dist /tmp/web_build + +# Backend +- name: backend_fetch_deps + image: rust + volumes: + - name: rust_registry + path: /usr/local/cargo/registry + commands: + - cd matrixgw_backend + - cargo fetch + +- name: backend_code_quality + image: rust + volumes: + - name: rust_registry + path: /usr/local/cargo/registry + depends_on: + - backend_fetch_deps + commands: + - cd matrixgw_backend + - rustup component add clippy + - cargo clippy -- -D warnings + - cargo clippy --example api_curl -- -D warnings + +- name: backend_test + image: rust + volumes: + - name: rust_registry + path: /usr/local/cargo/registry + depends_on: + - backend_code_quality + commands: + - cd matrixgw_backend + - cargo test + + +- name: backend_build + image: rust + when: + event: + - tag + volumes: + - name: rust_registry + path: /usr/local/cargo/registry + - name: web_app + path: /tmp/web_build + - name: release + path: /tmp/release + depends_on: + - backend_test + - web_build + commands: + - cd matrixgw_backend + - mv /tmp/web_build/dist static + - cargo build --release + - cargo build --release --example api_curl + - ls -lah target/release/matrixgw_backend target/release/examples/api_curl + - cp target/release/matrixgw_backend target/release/examples/api_curl /tmp/release + +# Release +- name: gitea_release + image: plugins/gitea-release + depends_on: + - backend_build + when: + event: + - tag + volumes: + - name: release + path: /tmp/release + environment: + PLUGIN_API_KEY: + from_secret: API_KEY + settings: + base_url: https://gitea.communiquons.org + files: /tmp/release/* + checksum: sha512 + +volumes: +- name: rust_registry + temp: {} +- name: web_app + temp: {} +- name: release + temp: {} \ No newline at end of file From 30518f3ca3002248fcf7d9bbaf81613595ce896d Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 09:46:51 +0100 Subject: [PATCH 114/124] Fix Web build --- .drone.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index e81ad07..611b3a0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,7 +11,7 @@ steps: - name: web_app path: /tmp/web_build commands: - - cd matrixgw_backend + - cd matrixgw_frontend - npm install - npm run lint - npm run build From 1090a59aaf9fecb11938ccc0ea05a9af2b709ad6 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 09:48:03 +0100 Subject: [PATCH 115/124] Quick ESLint issues fix --- matrixgw_frontend/src/utils/RoomEventsManager.ts | 4 ++-- matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 4ac527d..9c20f10 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -158,7 +158,7 @@ export class RoomEventsManager { } // First, process redactions to skip redacted events - let redacted = new Set( + const redacted = new Set( this.events .map((e) => e.data.type === "m.room.redaction" ? e.data.redacts : undefined @@ -186,7 +186,7 @@ export class RoomEventsManager { // Else it is a new message; update receipts if needed else { - let userReceipt = receiptsUsersMap.get(evt.sender); + const userReceipt = receiptsUsersMap.get(evt.sender); // Create fake receipt if none is available if (!userReceipt) diff --git a/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx b/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx index 5cce966..11673e9 100644 --- a/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx +++ b/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx @@ -18,7 +18,7 @@ function getInitialFavicon(): HTMLLinkElement[] { return icons; } -let iconPath = getInitialFavicon()[0].getAttribute("href")!; +const iconPath = getInitialFavicon()[0].getAttribute("href")!; export function AppIconModifier(p: { numberUnread: number; From bbf558bbf98135db2f090c479e623f7bbbf9602c Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 11:16:14 +0100 Subject: [PATCH 116/124] WIP ESLint fixes --- matrixgw_frontend/src/api/ApiClient.ts | 9 +-- .../src/routes/MatrixAuthCallback.tsx | 2 +- matrixgw_frontend/src/utils/DateUtils.ts | 61 +++++++++++++++++ .../src/utils/RoomEventsManager.ts | 2 +- matrixgw_frontend/src/widgets/TimeWidget.tsx | 67 ++----------------- .../widgets/dashboard/DashboardSidebar.tsx | 6 +- .../widgets/messages/MainMessagesWidget.tsx | 6 +- .../src/widgets/messages/MatrixWS.tsx | 2 +- .../src/widgets/messages/RoomMessagesList.tsx | 7 +- .../src/widgets/messages/TypingNotice.tsx | 2 +- 10 files changed, 86 insertions(+), 78 deletions(-) diff --git a/matrixgw_frontend/src/api/ApiClient.ts b/matrixgw_frontend/src/api/ApiClient.ts index fdb7da3..fcd4f11 100644 --- a/matrixgw_frontend/src/api/ApiClient.ts +++ b/matrixgw_frontend/src/api/ApiClient.ts @@ -4,21 +4,21 @@ interface RequestParams { uri: string; method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT"; allowFail?: boolean; - jsonData?: any; + jsonData?: unknown; formData?: FormData; upProgress?: (progress: number) => void; downProgress?: (e: { progress: number; total: number }) => void; } interface APIResponse { - data: any; + data: unknown; status: number; } export class ApiError extends Error { public code: number; - public data: number; - constructor(message: string, code: number, data: any) { + public data: unknown; + constructor(message: string, code: number, data: unknown) { super(`HTTP status: ${code}\nMessage: ${message}\nData=${data}`); this.code = code; this.data = data; @@ -57,6 +57,7 @@ export class APIClient { */ static async exec(args: RequestParams): Promise { let body: string | undefined | FormData = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const headers: any = {}; // JSON request diff --git a/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx index 3eafadc..1d5aa19 100644 --- a/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx +++ b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx @@ -41,7 +41,7 @@ export function MatrixAuthCallback(): React.ReactElement { }; load(); - }, [code, state]); + }, [code, info, navigate, snackbar, state]); if (error) return ( diff --git a/matrixgw_frontend/src/utils/DateUtils.ts b/matrixgw_frontend/src/utils/DateUtils.ts index 7c680b9..0bf9610 100644 --- a/matrixgw_frontend/src/utils/DateUtils.ts +++ b/matrixgw_frontend/src/utils/DateUtils.ts @@ -1,3 +1,5 @@ +import { format } from "date-and-time"; + /** * Get UNIX time * @@ -15,3 +17,62 @@ export function time(): number { export function timeMs(): number { return new Date().getTime(); } + +export function formatDateTime(time: number): string { + const t = new Date(); + t.setTime(1000 * time); + return format(t, "DD/MM/YYYY HH:mm:ss"); +} + +export function formatDate(time: number): string { + const t = new Date(); + t.setTime(1000 * time); + return format(t, "DD/MM/YYYY"); +} + +export function timeDiff(a: number, b: number): string { + let diff = b - a; + + if (diff === 0) return "now"; + if (diff === 1) return "1 second"; + + if (diff < 60) { + return `${diff} seconds`; + } + + diff = Math.floor(diff / 60); + + if (diff === 1) return "1 minute"; + if (diff < 60) { + return `${diff} minutes`; + } + + diff = Math.floor(diff / 60); + + if (diff === 1) return "1 hour"; + if (diff < 24) { + return `${diff} hours`; + } + + const diffDays = Math.floor(diff / 24); + + if (diffDays === 1) return "1 day"; + if (diffDays < 31) { + return `${diffDays} days`; + } + + diff = Math.floor(diffDays / 31); + + if (diff < 12) { + return `${diff} month`; + } + + const diffYears = Math.floor(diffDays / 365); + + if (diffYears === 1) return "1 year"; + return `${diffYears} years`; +} + +export function timeDiffFromNow(t: number): string { + return timeDiff(t, time()); +} diff --git a/matrixgw_frontend/src/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts index 9c20f10..2efb88a 100644 --- a/matrixgw_frontend/src/utils/RoomEventsManager.ts +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -236,7 +236,7 @@ export class RoomEventsManager { // Adapt receipts to be event-indexed this.receiptsEventsMap.clear(); - for (const [_userId, receipt] of receiptsUsersMap) { + for (const receipt of [...receiptsUsersMap.values()]) { if (!this.receiptsEventsMap.has(receipt.event_id)) this.receiptsEventsMap.set(receipt.event_id, [receipt]); else this.receiptsEventsMap.get(receipt.event_id)!.push(receipt); diff --git a/matrixgw_frontend/src/widgets/TimeWidget.tsx b/matrixgw_frontend/src/widgets/TimeWidget.tsx index 253a0e0..1813af3 100644 --- a/matrixgw_frontend/src/widgets/TimeWidget.tsx +++ b/matrixgw_frontend/src/widgets/TimeWidget.tsx @@ -1,65 +1,10 @@ import { Tooltip } from "@mui/material"; -import { format } from "date-and-time"; -import { time } from "../utils/DateUtils"; - -export function formatDateTime(time: number): string { - const t = new Date(); - t.setTime(1000 * time); - return format(t, "DD/MM/YYYY HH:mm:ss"); -} - -export function formatDate(time: number): string { - const t = new Date(); - t.setTime(1000 * time); - return format(t, "DD/MM/YYYY"); -} - -export function timeDiff(a: number, b: number): string { - let diff = b - a; - - if (diff === 0) return "now"; - if (diff === 1) return "1 second"; - - if (diff < 60) { - return `${diff} seconds`; - } - - diff = Math.floor(diff / 60); - - if (diff === 1) return "1 minute"; - if (diff < 60) { - return `${diff} minutes`; - } - - diff = Math.floor(diff / 60); - - if (diff === 1) return "1 hour"; - if (diff < 24) { - return `${diff} hours`; - } - - const diffDays = Math.floor(diff / 24); - - if (diffDays === 1) return "1 day"; - if (diffDays < 31) { - return `${diffDays} days`; - } - - diff = Math.floor(diffDays / 31); - - if (diff < 12) { - return `${diff} month`; - } - - const diffYears = Math.floor(diffDays / 365); - - if (diffYears === 1) return "1 year"; - return `${diffYears} years`; -} - -export function timeDiffFromNow(t: number): string { - return timeDiff(t, time()); -} +import { + formatDateTime, + formatDate, + timeDiff, + timeDiffFromNow, +} from "../utils/DateUtils"; export function TimeWidget(p: { time?: number; diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx index c733355..6c5f7a1 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx @@ -62,7 +62,7 @@ export default function DashboardSidebar({ if (!isOverSmViewport) { setExpanded(false); } - }, [expanded, setExpanded, isOverSmViewport]); + }, [setExpanded, isOverSmViewport]); const hasDrawerTransitions = isOverSmViewport && isOverMdViewport; @@ -159,7 +159,7 @@ export default function DashboardSidebar({ }, }; }, - [expanded, !expanded] + [expanded] ); const sidebarContextValue = React.useMemo(() => { @@ -168,7 +168,7 @@ export default function DashboardSidebar({ fullyExpanded: isFullyExpanded, hasDrawerTransitions, }; - }, [handlePageItemClick, !expanded, isFullyExpanded, hasDrawerTransitions]); + }, [handlePageItemClick, isFullyExpanded, hasDrawerTransitions]); return ( diff --git a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx index d5c60a8..9437353 100644 --- a/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -43,7 +43,7 @@ export function MainMessageWidget(): React.ReactElement { ready={!!rooms && !!users} errMsg="Failed to initialize messaging component!" build={() => ( - <_MainMessageWidget + setRooms((r) => cb(r!))} @@ -53,7 +53,7 @@ export function MainMessageWidget(): React.ReactElement { ); } -function _MainMessageWidget(p: { +function MainMessageWidgetInner(p: { rooms: Room[]; users: UsersMap; onRoomsListUpdate: (cb: (a: Room[]) => Room[]) => void; @@ -79,7 +79,7 @@ function _MainMessageWidget(p: { [p.rooms] ); - const [_refreshCount, setRefreshCount] = React.useState(0); + const setRefreshCount = React.useState(0)[1]; const [roomMgr, setRoomMgr] = React.useState(); const loadRoom = async () => { diff --git a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx index 8e376cb..ea9061a 100644 --- a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx +++ b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx @@ -75,7 +75,7 @@ export function MatrixWS(p: { }; return () => ws.close(); - }, [connCount]); + }, [connCount, snackbar]); return ( diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index 4553ab6..e0e2cf7 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -49,10 +49,11 @@ export function RoomMessagesList(p: { const messagesEndRef = React.createRef(); // Automatically scroll to bottom when number of messages change + const lastEventId = p.manager.messages.at(-1)?.event_id; React.useEffect(() => { if (messagesEndRef) messagesEndRef.current?.scrollIntoView({ behavior: "instant" }); - }, [p.manager.messages.at(-1)?.event_id]); + }, [lastEventId, messagesEndRef]); // Watch scroll to detect when user reach the top to load older messages const handleScroll = async () => { @@ -185,8 +186,8 @@ function RoomMessage(p: { try { await MatrixApiEvent.DeleteEvent(p.room, p.message.event_id); } catch (e) { - console.error(`Failed to delete message!`, e), - alert(`Failed to delete message!${e}`); + console.error(`Failed to delete message!`, e); + alert(`Failed to delete message!${e}`); } }; diff --git a/matrixgw_frontend/src/widgets/messages/TypingNotice.tsx b/matrixgw_frontend/src/widgets/messages/TypingNotice.tsx index 4223811..578718d 100644 --- a/matrixgw_frontend/src/widgets/messages/TypingNotice.tsx +++ b/matrixgw_frontend/src/widgets/messages/TypingNotice.tsx @@ -17,7 +17,7 @@ export function TypingNotice(p: { p.manager.typingUsers.includes(u.user_id) && u.user_id !== user.info.matrix_user_id ), - [p.manager.typingUsers] + [p.manager.typingUsers, p.users, user.info.matrix_user_id] ); if (users.length === 0) return <>; From f6568cf059b9f8b168864e1cd7462316036e1327 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 14:38:52 +0100 Subject: [PATCH 117/124] Fix ESLint issues --- matrixgw_frontend/eslint.config.js | 23 +++++---- .../src/routes/MatrixLinkRoute.tsx | 2 +- matrixgw_frontend/src/routes/WSDebugRoute.tsx | 48 ++++--------------- .../src/theme/customizations/dataDisplay.tsx | 1 - .../src/theme/customizations/feedback.tsx | 1 - .../src/theme/customizations/inputs.tsx | 1 - .../src/theme/customizations/navigation.tsx | 1 - .../src/theme/customizations/surfaces.ts | 1 - .../src/theme/themePrimitives.ts | 6 +-- matrixgw_frontend/src/widgets/AsyncWidget.tsx | 4 +- 10 files changed, 27 insertions(+), 61 deletions(-) diff --git a/matrixgw_frontend/eslint.config.js b/matrixgw_frontend/eslint.config.js index b19330b..2606d73 100644 --- a/matrixgw_frontend/eslint.config.js +++ b/matrixgw_frontend/eslint.config.js @@ -1,23 +1,26 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ js.configs.recommended, tseslint.configs.recommended, - reactHooks.configs['recommended-latest'], + reactHooks.configs["recommended-latest"], reactRefresh.configs.vite, ], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, + rules: { + "react-refresh/only-export-components": "off", + }, }, -]) +]); diff --git a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx index 1c57e5b..0125fad 100644 --- a/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx +++ b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx @@ -257,7 +257,7 @@ function SyncThreadStatus(): React.ReactElement { const interval = setInterval(loadStatus, 1000); return () => clearInterval(interval); - }, []); + }); return ( <> diff --git a/matrixgw_frontend/src/routes/WSDebugRoute.tsx b/matrixgw_frontend/src/routes/WSDebugRoute.tsx index fa6fb67..b572c3a 100644 --- a/matrixgw_frontend/src/routes/WSDebugRoute.tsx +++ b/matrixgw_frontend/src/routes/WSDebugRoute.tsx @@ -1,64 +1,34 @@ import React from "react"; import { JsonView, darkStyles } from "react-json-view-lite"; import "react-json-view-lite/dist/index.css"; -import { WsApi, type WsMessage } from "../api/WsApi"; -import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider"; -import { time } from "../utils/DateUtils"; +import { type WsMessage } from "../api/WsApi"; import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer"; +import { MatrixWS, WSState } from "../widgets/messages/MatrixWS"; import { NotLinkedAccountMessage } from "../widgets/NotLinkedAccountMessage"; -const State = { - Closed: "Closed", - Connected: "Connected", - Error: "Error", -} as const; - type TimestampedMessages = WsMessage & { time: number }; export function WSDebugRoute(): React.ReactElement { const user = useUserInfo(); - if (!user.info.matrix_account_connected) return ; - - const snackbar = useSnackbar(); - - const [state, setState] = React.useState(State.Closed); - const wsRef = React.useRef(undefined); + const [state, setState] = React.useState(WSState.Closed); const [messages, setMessages] = React.useState([]); - React.useEffect(() => { - const ws = new WebSocket(WsApi.WsURL); - wsRef.current = ws; + const handleMessage = (msg: WsMessage) => { + setMessages((l) => [...l, msg]); + }; - ws.onopen = () => setState(State.Connected); - ws.onerror = (e) => { - console.error(`WS Debug error!`, e); - snackbar(`WebSocket error! ${e}`); - setState(State.Error); - }; - ws.onclose = () => { - setState(State.Closed); - wsRef.current = undefined; - }; - - ws.onmessage = (msg) => { - const dec = JSON.parse(msg.data); - setMessages((l) => { - return [{ time: time(), ...dec }, ...l]; - }); - }; - - return () => ws.close(); - }, []); + if (!user.info.matrix_account_connected) return ; return (
    State:{" "} - + {state} +
    {messages.map((msg, id) => (
    diff --git a/matrixgw_frontend/src/theme/customizations/dataDisplay.tsx b/matrixgw_frontend/src/theme/customizations/dataDisplay.tsx index c23166a..a3ba6b6 100644 --- a/matrixgw_frontend/src/theme/customizations/dataDisplay.tsx +++ b/matrixgw_frontend/src/theme/customizations/dataDisplay.tsx @@ -6,7 +6,6 @@ import { svgIconClasses } from "@mui/material/SvgIcon"; import { typographyClasses } from "@mui/material/Typography"; import { gray, green, red } from "../themePrimitives"; -/* eslint-disable import/prefer-default-export */ export const dataDisplayCustomizations: Components = { MuiList: { styleOverrides: { diff --git a/matrixgw_frontend/src/theme/customizations/feedback.tsx b/matrixgw_frontend/src/theme/customizations/feedback.tsx index b8dc04b..29a7e94 100644 --- a/matrixgw_frontend/src/theme/customizations/feedback.tsx +++ b/matrixgw_frontend/src/theme/customizations/feedback.tsx @@ -1,7 +1,6 @@ import { type Theme, alpha, type Components } from "@mui/material/styles"; import { gray, orange } from "../themePrimitives"; -/* eslint-disable import/prefer-default-export */ export const feedbackCustomizations: Components = { MuiAlert: { styleOverrides: { diff --git a/matrixgw_frontend/src/theme/customizations/inputs.tsx b/matrixgw_frontend/src/theme/customizations/inputs.tsx index d30fcf2..21c2b09 100644 --- a/matrixgw_frontend/src/theme/customizations/inputs.tsx +++ b/matrixgw_frontend/src/theme/customizations/inputs.tsx @@ -8,7 +8,6 @@ import CheckRoundedIcon from "@mui/icons-material/CheckRounded"; import RemoveRoundedIcon from "@mui/icons-material/RemoveRounded"; import { gray, brand } from "../themePrimitives"; -/* eslint-disable import/prefer-default-export */ export const inputsCustomizations: Components = { MuiButtonBase: { defaultProps: { diff --git a/matrixgw_frontend/src/theme/customizations/navigation.tsx b/matrixgw_frontend/src/theme/customizations/navigation.tsx index 2b1a584..6b5cb2d 100644 --- a/matrixgw_frontend/src/theme/customizations/navigation.tsx +++ b/matrixgw_frontend/src/theme/customizations/navigation.tsx @@ -9,7 +9,6 @@ import { tabClasses } from "@mui/material/Tab"; import UnfoldMoreRoundedIcon from "@mui/icons-material/UnfoldMoreRounded"; import { gray, brand } from "../themePrimitives"; -/* eslint-disable import/prefer-default-export */ export const navigationCustomizations: Components = { MuiMenuItem: { styleOverrides: { diff --git a/matrixgw_frontend/src/theme/customizations/surfaces.ts b/matrixgw_frontend/src/theme/customizations/surfaces.ts index 38bf7ed..9dbe938 100644 --- a/matrixgw_frontend/src/theme/customizations/surfaces.ts +++ b/matrixgw_frontend/src/theme/customizations/surfaces.ts @@ -1,7 +1,6 @@ import { alpha, type Theme, type Components } from "@mui/material/styles"; import { gray } from "../themePrimitives"; -/* eslint-disable import/prefer-default-export */ export const surfacesCustomizations: Components = { MuiAccordion: { defaultProps: { diff --git a/matrixgw_frontend/src/theme/themePrimitives.ts b/matrixgw_frontend/src/theme/themePrimitives.ts index 1e95864..b924844 100644 --- a/matrixgw_frontend/src/theme/themePrimitives.ts +++ b/matrixgw_frontend/src/theme/themePrimitives.ts @@ -24,8 +24,6 @@ declare module "@mui/material/styles" { 900: string; } - interface PaletteColor extends ColorRange {} - interface Palette { baseShadow: string; } @@ -405,10 +403,10 @@ export const shape = { borderRadius: 8, }; -// @ts-ignore const defaultShadows: Shadows = [ "none", "var(--template-palette-baseShadow)", ...defaultTheme.shadows.slice(2), -]; +] as never; + export const shadows = defaultShadows; diff --git a/matrixgw_frontend/src/widgets/AsyncWidget.tsx b/matrixgw_frontend/src/widgets/AsyncWidget.tsx index bc7e036..1a4701c 100644 --- a/matrixgw_frontend/src/widgets/AsyncWidget.tsx +++ b/matrixgw_frontend/src/widgets/AsyncWidget.tsx @@ -10,7 +10,7 @@ const State = { type State = keyof typeof State; export function AsyncWidget(p: { - loadKey: any; + loadKey: unknown; load: () => Promise; errMsg: string; build: () => React.ReactElement; @@ -19,7 +19,7 @@ export function AsyncWidget(p: { }): React.ReactElement { const [state, setState] = useState(State.Loading); - const counter = useRef(null); + const counter = useRef(null); const load = async () => { try { From fb35fca56eacc5b5e7d66213da8a84132449704d Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 14:53:06 +0100 Subject: [PATCH 118/124] Fix build issues --- .drone.yml | 2 ++ matrixgw_frontend/package-lock.json | 35 ++++++++++--------- matrixgw_frontend/src/api/AuthApi.ts | 4 +-- matrixgw_frontend/src/api/MatrixLinkApi.ts | 2 +- matrixgw_frontend/src/api/MatrixSyncApi.ts | 2 +- matrixgw_frontend/src/api/ServerApi.ts | 2 +- matrixgw_frontend/src/api/TokensApi.ts | 14 ++++---- .../src/api/matrix/MatrixApiEvent.ts | 2 +- .../src/api/matrix/MatrixApiProfile.ts | 4 +-- .../src/api/matrix/MatrixApiRoom.ts | 4 +-- 10 files changed, 37 insertions(+), 34 deletions(-) diff --git a/.drone.yml b/.drone.yml index 611b3a0..fde8374 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,6 +11,8 @@ steps: - name: web_app path: /tmp/web_build commands: + - node -v + - npm -v - cd matrixgw_frontend - npm install - npm run lint diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index 254aaa2..a459209 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -73,6 +73,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -422,6 +423,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -465,6 +467,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -856,6 +859,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.5", @@ -966,6 +970,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz", "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.5", @@ -1642,6 +1647,7 @@ "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1663,6 +1669,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1732,6 +1739,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -1984,6 +1992,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2127,6 +2136,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2311,7 +2321,8 @@ "version": "1.11.19", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -2416,6 +2427,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3676,6 +3688,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3685,6 +3698,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -4033,6 +4047,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4093,6 +4108,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4189,6 +4205,7 @@ "integrity": "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@oxc-project/runtime": "0.92.0", "fdir": "^6.5.0", @@ -4283,6 +4300,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4323,21 +4341,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/matrixgw_frontend/src/api/AuthApi.ts b/matrixgw_frontend/src/api/AuthApi.ts index bcf6f3d..5d5c926 100644 --- a/matrixgw_frontend/src/api/AuthApi.ts +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -45,7 +45,7 @@ export class AuthApi { uri: "/auth/start_oidc", method: "GET", }) - ).data; + ).data as { url: string }; } /** @@ -70,7 +70,7 @@ export class AuthApi { uri: "/auth/info", method: "GET", }) - ).data; + ).data as UserInfo; } /** diff --git a/matrixgw_frontend/src/api/MatrixLinkApi.ts b/matrixgw_frontend/src/api/MatrixLinkApi.ts index 12507b7..7fc1f83 100644 --- a/matrixgw_frontend/src/api/MatrixLinkApi.ts +++ b/matrixgw_frontend/src/api/MatrixLinkApi.ts @@ -10,7 +10,7 @@ export class MatrixLinkApi { uri: "/matrix_link/start_auth", method: "POST", }) - ).data; + ).data as { url: string }; } /** diff --git a/matrixgw_frontend/src/api/MatrixSyncApi.ts b/matrixgw_frontend/src/api/MatrixSyncApi.ts index 8e8999c..509ff85 100644 --- a/matrixgw_frontend/src/api/MatrixSyncApi.ts +++ b/matrixgw_frontend/src/api/MatrixSyncApi.ts @@ -29,6 +29,6 @@ export class MatrixSyncApi { method: "GET", uri: "/matrix_sync/status", }); - return res.data.started; + return (res.data as { started: boolean }).started; } } diff --git a/matrixgw_frontend/src/api/ServerApi.ts b/matrixgw_frontend/src/api/ServerApi.ts index eb7d24d..50f52a1 100644 --- a/matrixgw_frontend/src/api/ServerApi.ts +++ b/matrixgw_frontend/src/api/ServerApi.ts @@ -35,7 +35,7 @@ export class ServerApi { uri: "/server/config", method: "GET", }) - ).data; + ).data as ServerConfig; } /** diff --git a/matrixgw_frontend/src/api/TokensApi.ts b/matrixgw_frontend/src/api/TokensApi.ts index acddaf6..92f23c2 100644 --- a/matrixgw_frontend/src/api/TokensApi.ts +++ b/matrixgw_frontend/src/api/TokensApi.ts @@ -28,7 +28,7 @@ export class TokensApi { uri: "/tokens", method: "GET", }) - ).data; + ).data as Token[]; } /** @@ -41,18 +41,16 @@ export class TokensApi { method: "POST", jsonData: t, }) - ).data; + ).data as TokenWithSecret; } /** * Delete a token */ static async Delete(t: Token): Promise { - return ( - await APIClient.exec({ - uri: `/token/${t.id}`, - method: "DELETE", - }) - ).data; + await APIClient.exec({ + uri: `/token/${t.id}`, + method: "DELETE", + }); } } diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts index 497dc0d..34d31d7 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -74,7 +74,7 @@ export class MatrixApiEvent { `/matrix/room/${encodeURIComponent(room.id)}/events` + (from ? `?from=${from}` : ""), }) - ).data; + ).data as MatrixEventsList; } /** diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts b/matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts index ccb39db..3301c60 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiProfile.ts @@ -13,13 +13,13 @@ export class MatrixApiProfile { * Get multiple profiles information */ static async GetMultiple(ids: string[]): Promise { - const list: UserProfile[] = ( + const list = ( await APIClient.exec({ method: "POST", uri: "/matrix/profile/get_multiple", jsonData: ids, }) - ).data; + ).data as UserProfile[]; return new Map(list.map((e) => [e.user_id, e])); } diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts index 9809143..3d5d116 100644 --- a/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts +++ b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts @@ -57,7 +57,7 @@ export class MatrixApiRoom { method: "GET", uri: "/matrix/room/joined", }) - ).data; + ).data as Room[]; } /** @@ -69,6 +69,6 @@ export class MatrixApiRoom { method: "GET", uri: `/matrix/room/${room.id}/receipts`, }) - ).data; + ).data as Receipt[]; } } From dfcf764a9b66a9fc4b28768490cc195b74f7cdd9 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 15:00:58 +0100 Subject: [PATCH 119/124] Updated backend dependencies --- matrixgw_backend/Cargo.lock | 332 ++++++++++++++++++------------------ matrixgw_backend/Cargo.toml | 10 +- 2 files changed, 168 insertions(+), 174 deletions(-) diff --git a/matrixgw_backend/Cargo.lock b/matrixgw_backend/Cargo.lock index 8666560..c02a2fa 100644 --- a/matrixgw_backend/Cargo.lock +++ b/matrixgw_backend/Cargo.lock @@ -188,9 +188,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.11.0" +version = "4.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" +checksum = "1654a77ba142e37f049637a3e5685f864514af11fcbc51cb51eb6596afe5b8d6" dependencies = [ "actix-codec", "actix-http", @@ -223,7 +223,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.5.10", + "socket2 0.6.1", "time", "tracing", "url", @@ -367,22 +367,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -470,9 +470,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.32" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473" dependencies = [ "compression-codecs", "compression-core", @@ -731,15 +731,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "bytesize" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" [[package]] name = "bytestring" @@ -761,9 +761,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.44" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "jobserver", @@ -829,7 +829,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -845,9 +845,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -855,9 +855,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -916,9 +916,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad" dependencies = [ "compression-core", "flate2", @@ -927,9 +927,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.29" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" [[package]] name = "concurrent-queue" @@ -961,6 +961,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.16.2" @@ -1067,9 +1076,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1295,21 +1304,23 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ + "convert_case", "proc-macro2", "quote", + "rustc_version", "syn", "unicode-xid", ] @@ -1370,12 +1381,12 @@ dependencies = [ [[package]] name = "ed25519-compact" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +checksum = "33ce99a9e19c84beb4cc35ece85374335ccc398240712114c85038319ed709bd" dependencies = [ "ct-codecs", - "getrandom 0.2.16", + "getrandom 0.3.4", ] [[package]] @@ -1578,9 +1589,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" @@ -1739,9 +1750,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1768,9 +1779,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1861,7 +1874,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http 1.4.0", "indexmap", "slab", "tokio", @@ -1888,9 +1901,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hashlink" @@ -1910,7 +1923,7 @@ dependencies = [ "base64 0.22.1", "bytes", "headers-core", - "http 1.3.1", + "http 1.4.0", "httpdate", "mime", "sha1", @@ -1922,7 +1935,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -2016,12 +2029,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -2041,7 +2053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -2052,7 +2064,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body", "pin-project-lite", ] @@ -2071,16 +2083,16 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "h2 0.4.12", - "http 1.3.1", + "http 1.4.0", "http-body", "httparse", "itoa", @@ -2097,7 +2109,7 @@ version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", + "http 1.4.0", "hyper", "hyper-util", "rustls", @@ -2125,16 +2137,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body", "hyper", "ipnet", @@ -2358,12 +2370,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -2398,9 +2410,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -2438,22 +2450,22 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", @@ -2472,9 +2484,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -2604,9 +2616,9 @@ checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libm" @@ -2678,9 +2690,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" @@ -2836,7 +2848,7 @@ dependencies = [ "futures-core", "futures-util", "gloo-timers", - "http 1.3.1", + "http 1.4.0", "imbl", "indexmap", "itertools 0.14.0", @@ -3224,9 +3236,9 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ "lazy_static", "libm", @@ -3293,7 +3305,7 @@ dependencies = [ "base64 0.22.1", "chrono", "getrandom 0.2.16", - "http 1.3.1", + "http 1.4.0", "rand 0.8.5", "reqwest", "serde", @@ -3330,9 +3342,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.74" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", @@ -3362,9 +3374,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.110" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -3431,7 +3443,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -3693,9 +3705,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -3920,7 +3932,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.4.12", - "http 1.3.1", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -3999,9 +4011,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest", @@ -4046,7 +4058,7 @@ dependencies = [ "assign", "bytes", "date_header", - "http 1.3.1", + "http 1.4.0", "js_int", "js_option", "maplit", @@ -4071,7 +4083,7 @@ dependencies = [ "bytes", "form_urlencoded", "getrandom 0.2.16", - "http 1.3.1", + "http 1.4.0", "indexmap", "js-sys", "js_int", @@ -4126,7 +4138,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecbc887ba1292e48e6363b29e0dec4571b52d2b5102ebf60068105efadaa6e0a" dependencies = [ "headers", - "http 1.3.1", + "http 1.4.0", "http-auth", "httparse", "js_int", @@ -4282,9 +4294,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.34" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "rustls-pki-types", @@ -4295,9 +4307,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "zeroize", ] @@ -4540,9 +4552,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -4706,9 +4718,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -5094,9 +5106,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -5210,14 +5222,14 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ "bitflags", "bytes", "futures-util", - "http 1.3.1", + "http 1.4.0", "http-body", "iri-string", "pin-project-lite", @@ -5240,9 +5252,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "log", "pin-project-lite", @@ -5252,9 +5264,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -5263,9 +5275,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -5284,9 +5296,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -5354,6 +5366,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -5420,13 +5438,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -5523,9 +5541,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -5536,9 +5554,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -5549,9 +5567,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5559,9 +5577,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -5572,9 +5590,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -5594,9 +5612,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -5614,9 +5632,9 @@ dependencies = [ [[package]] name = "wildmatch" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b7d07a236abaef6607536ccfaf19b396dbe3f5110ddb73d39f4562902ed382" +checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68" [[package]] name = "winapi" @@ -5657,9 +5675,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -5684,12 +5702,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" @@ -5698,22 +5710,13 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -5722,16 +5725,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -5740,7 +5734,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -5776,7 +5770,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -5801,7 +5795,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.2.1", + "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -5910,9 +5904,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -5972,18 +5966,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", diff --git a/matrixgw_backend/Cargo.toml b/matrixgw_backend/Cargo.toml index 454ea41..dedead8 100644 --- a/matrixgw_backend/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -5,24 +5,24 @@ edition = "2024" [dependencies] env_logger = "0.11.8" -log = "0.4.28" -clap = { version = "4.5.51", features = ["derive", "env"] } +log = "0.4.29" +clap = { version = "4.5.53", features = ["derive", "env"] } lazy_static = "1.5.0" anyhow = "1.0.100" serde = { version = "1.0.228", features = ["derive"] } tokio = { version = "1.48.0", features = ["full"] } -actix-web = "4.11.0" +actix-web = "4.12.1" actix-session = { version = "0.11.0", features = ["redis-session"] } actix-remote-ip = "0.1.0" actix-cors = "0.7.1" light-openid = "1.0.4" -bytes = "1.10.1" +bytes = "1.11.0" sha2 = "0.10.9" base16ct = { version = "0.3.0", features = ["alloc"] } futures-util = "0.3.31" jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] } thiserror = "2.0.17" -uuid = { version = "1.18.1", features = ["v4", "serde"] } +uuid = { version = "1.19.0", features = ["v4", "serde"] } ipnet = { version = "2.11.0", features = ["serde"] } rand = "0.9.2" hex = "0.4.3" From f087b27b53f95447d0dae54dbe48b28252a83758 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 15:13:01 +0100 Subject: [PATCH 120/124] Updated frontend dependencies --- matrixgw_frontend/eslint.config.js | 2 +- matrixgw_frontend/package-lock.json | 970 ++++++------------ matrixgw_frontend/package.json | 36 +- .../src/routes/APITokensRoute.tsx | 6 +- matrixgw_frontend/src/widgets/AsyncWidget.tsx | 14 +- 5 files changed, 360 insertions(+), 668 deletions(-) diff --git a/matrixgw_frontend/eslint.config.js b/matrixgw_frontend/eslint.config.js index 2606d73..7da0abe 100644 --- a/matrixgw_frontend/eslint.config.js +++ b/matrixgw_frontend/eslint.config.js @@ -12,7 +12,7 @@ export default defineConfig([ extends: [ js.configs.recommended, tseslint.configs.recommended, - reactHooks.configs["recommended-latest"], + reactHooks.configs.flat.recommended, reactRefresh.configs.vite, ], languageOptions: { diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index a459209..de031d4 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -10,14 +10,14 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@fontsource/roboto": "^5.2.8", + "@fontsource/roboto": "^5.2.9", "@mdi/js": "^7.4.47", "@mdi/react": "^1.6.1", - "@mui/icons-material": "^7.3.5", - "@mui/material": "^7.3.5", - "@mui/x-data-grid": "^8.18.0", - "@mui/x-date-pickers": "^8.17.0", - "date-and-time": "^4.1.0", + "@mui/icons-material": "^7.3.6", + "@mui/material": "^7.3.6", + "@mui/x-data-grid": "^8.20.0", + "@mui/x-date-pickers": "^8.19.0", + "date-and-time": "^4.1.1", "dayjs": "^1.11.19", "emoji-picker-react": "^4.16.1", "is-cidr": "^6.0.1", @@ -26,21 +26,21 @@ "react-dom": "^19.1.1", "react-favicon": "^2.0.7", "react-json-view-lite": "^2.5.0", - "react-router": "^7.9.5" + "react-router": "^7.10.0" }, "devDependencies": { - "@eslint/js": "^9.36.0", - "@types/node": "^24.6.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.4", - "eslint": "^9.36.0", - "eslint-plugin-react-hooks": "^5.2.0", + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.22", - "globals": "^16.4.0", + "globals": "^16.5.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.45.0", - "vite": "npm:rolldown-vite@7.1.14" + "typescript-eslint": "^8.48.1", + "vite": "npm:rolldown-vite@7.2.10" } }, "node_modules/@babel/code-frame": { @@ -326,9 +326,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", - "integrity": "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "dev": true, "license": "MIT", "optional": true, @@ -338,9 +338,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", - "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "dev": true, "license": "MIT", "optional": true, @@ -583,7 +583,7 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-helpers/node_modules/@eslint/core": { + "node_modules/@eslint/core": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", @@ -596,19 +596,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@eslint/eslintrc": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", @@ -647,9 +634,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", "engines": { @@ -683,23 +670,10 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@fontsource/roboto": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.8.tgz", - "integrity": "sha512-oh9g4Cg3loVMz9MWeKWfDI+ooxxG1aRVetkiKIb2ESS2rrryGecQ/y4pAj4z5A5ebyw450dYRi/c4k/I3UBhHA==", + "version": "5.2.9", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.9.tgz", + "integrity": "sha512-ZTkyHiPk74B/aj8BZWbsxD5Yu+Lq+nR64eV4wirlrac2qXR7jYk2h6JlLYuOuoruTkGQWNw2fMuKNavw7/rg0w==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -819,9 +793,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz", - "integrity": "sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.6.tgz", + "integrity": "sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg==", "license": "MIT", "funding": { "type": "opencollective", @@ -829,9 +803,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.5.tgz", - "integrity": "sha512-LciL1GLMZ+VlzyHAALSVAR22t8IST4LCXmljcUSx2NOutgO2XnxdIp8ilFbeNf9wpo0iUFbAuoQcB7h+HHIf3A==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.6.tgz", + "integrity": "sha512-0FfkXEj22ysIq5pa41A2NbcAhJSvmcZQ/vcTIbjDsd6hlslG82k5BEBqqS0ZJprxwIL3B45qpJ+bPHwJPlF7uQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4" @@ -844,7 +818,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^7.3.5", + "@mui/material": "^7.3.6", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -855,17 +829,17 @@ } }, "node_modules/@mui/material": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", - "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz", + "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==", "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", - "@mui/core-downloads-tracker": "^7.3.5", - "@mui/system": "^7.3.5", - "@mui/types": "^7.4.8", - "@mui/utils": "^7.3.5", + "@mui/core-downloads-tracker": "^7.3.6", + "@mui/system": "^7.3.6", + "@mui/types": "^7.4.9", + "@mui/utils": "^7.3.6", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -884,7 +858,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^7.3.5", + "@mui/material-pigment-css": "^7.3.6", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -905,13 +879,13 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.5.tgz", - "integrity": "sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.6.tgz", + "integrity": "sha512-Ws9wZpqM+FlnbZXaY/7yvyvWQo1+02Tbx50mVdNmzWEi51C51y56KAbaDCYyulOOBL6BJxuaqG8rNNuj7ivVyw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", - "@mui/utils": "^7.3.5", + "@mui/utils": "^7.3.6", "prop-types": "^15.8.1" }, "engines": { @@ -932,9 +906,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.5.tgz", - "integrity": "sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.6.tgz", + "integrity": "sha512-+wiYbtvj+zyUkmDB+ysH6zRjuQIJ+CM56w0fEXV+VDNdvOuSywG+/8kpjddvvlfMLsaWdQe5oTuYGBcodmqGzQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", @@ -966,17 +940,17 @@ } }, "node_modules/@mui/system": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz", - "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.6.tgz", + "integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==", "license": "MIT", "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", - "@mui/private-theming": "^7.3.5", - "@mui/styled-engine": "^7.3.5", - "@mui/types": "^7.4.8", - "@mui/utils": "^7.3.5", + "@mui/private-theming": "^7.3.6", + "@mui/styled-engine": "^7.3.6", + "@mui/types": "^7.4.9", + "@mui/utils": "^7.3.6", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1007,9 +981,9 @@ } }, "node_modules/@mui/types": { - "version": "7.4.8", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.8.tgz", - "integrity": "sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==", + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.9.tgz", + "integrity": "sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4" @@ -1024,13 +998,13 @@ } }, "node_modules/@mui/utils": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.5.tgz", - "integrity": "sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==", + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.6.tgz", + "integrity": "sha512-jn+Ba02O6PiFs7nKva8R2aJJ9kJC+3kQ2R0BbKNY3KQQ36Qng98GnPRFTlbwYTdMD6hLEBKaMLUktyg/rTfd2w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", - "@mui/types": "^7.4.8", + "@mui/types": "^7.4.9", "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1054,15 +1028,15 @@ } }, "node_modules/@mui/x-data-grid": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.18.0.tgz", - "integrity": "sha512-g8y5EI3TNqrimHpH/Hv6u6i04cbvsqh39Tg4bZEhGq+SDxWp42iABlUvB7p+gtXfyd+IbmpfzUQ1hOCsHlTMZw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-8.20.0.tgz", + "integrity": "sha512-PXZOZjxEcD3XL26EQN+wBS8/f5dY2JhprHk0Cm8hjoapvuFYChDPl1hA3QrbU0rpWNIOKotsLrnNBpXNgzQzKQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", "@mui/utils": "^7.3.5", - "@mui/x-internals": "8.18.0", - "@mui/x-virtualizer": "0.2.8", + "@mui/x-internals": "8.19.0", + "@mui/x-virtualizer": "0.2.10", "clsx": "^2.1.1", "prop-types": "^15.8.1", "use-sync-external-store": "^1.6.0" @@ -1091,37 +1065,15 @@ } } }, - "node_modules/@mui/x-data-grid/node_modules/@mui/x-internals": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.18.0.tgz", - "integrity": "sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==", + "node_modules/@mui/x-date-pickers": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.19.0.tgz", + "integrity": "sha512-TQ4FsGUsiGJVs+Ie4q7nHXUmFqZADXL/1hVtZpOKsdr3WQXwpX7C5YmeakZGFR2NZnuv4snFj+WTee3kgyFbyQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", "@mui/utils": "^7.3.5", - "reselect": "^5.1.1", - "use-sync-external-store": "^1.6.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@mui/x-date-pickers": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.17.0.tgz", - "integrity": "sha512-mrrkTJ1+r6MsPnKH/N5lCNJHkP0dZc2Fvd8fp5tyxa0jRyzwbxJKsadXooccoJWp65Z2vUjUuctXYUmubYP/Sg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4", - "@mui/utils": "^7.3.3", - "@mui/x-internals": "8.17.0", + "@mui/x-internals": "8.19.0", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1180,13 +1132,13 @@ } }, "node_modules/@mui/x-internals": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.17.0.tgz", - "integrity": "sha512-KvmR0PPX1j2i44y0DXwzs45jIPMu/YZYXYy7xvzo+ZNdYebbW5LbVeG4zdEUnKHyOG02oHdI7MM9AxcZE16TBw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.19.0.tgz", + "integrity": "sha512-mMmiyJAN5fW27srXJjhXhXJa+w2xGO45rwcjws6OQc9rdXGdJqRXhBwJd+OT7J1xwSdFIIUhjZRTz1KAfCSGBg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", - "@mui/utils": "^7.3.3", + "@mui/utils": "^7.3.5", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, @@ -1202,14 +1154,14 @@ } }, "node_modules/@mui/x-virtualizer": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.2.8.tgz", - "integrity": "sha512-hCkhTg3BLLbf0SIw9Cx/NHTCUmbna+P5F2V+Bcv/9XiYhfzzmhYnm68+V6vOOhKVbV3j8JKsUEqcTC9K2Jpu8A==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@mui/x-virtualizer/-/x-virtualizer-0.2.10.tgz", + "integrity": "sha512-PGZeWFQzyw3IuxuoDT8NewwSseO6PxR86iC7WGMfmUwJ3s1Z2XL83PXlAxLeqJ4Oi9Y4+4wQ7m6bLAKg7VQ3kg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.4", "@mui/utils": "^7.3.5", - "@mui/x-internals": "8.18.0" + "@mui/x-internals": "8.19.0" }, "engines": { "node": ">=14.0.0" @@ -1223,83 +1175,23 @@ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@mui/x-virtualizer/node_modules/@mui/x-internals": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.18.0.tgz", - "integrity": "sha512-iM2SJALLo4kNqxTel8lfjIymYV9MgTa6021/rAlfdh/vwPMglaKyXQHrxkkWs2Eu/JFKkCKr5Fd34Gsdp63wIg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.28.4", - "@mui/utils": "^7.3.5", - "reselect": "^5.1.1", - "use-sync-external-store": "^1.6.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", + "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@oxc-project/runtime": { - "version": "0.92.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.92.0.tgz", - "integrity": "sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==", + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.101.0.tgz", + "integrity": "sha512-t3qpfVZIqSiLQ5Kqt/MC4Ge/WCOGrrcagAdzTcDaggupjiGxUx4nJF2v6wUCXWSzWHn5Ns7XLv13fCJEwCOERQ==", "dev": true, "license": "MIT", "engines": { @@ -1307,9 +1199,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.93.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.93.0.tgz", - "integrity": "sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==", + "version": "0.101.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.101.0.tgz", + "integrity": "sha512-nuFhqlUzJX+gVIPPfuE6xurd4lST3mdcWOhyK/rZO0B9XWMKm79SuszIQEnSMmmDhq1DC8WWVYGVd+6F93o1gQ==", "dev": true, "license": "MIT", "funding": { @@ -1327,9 +1219,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.41.tgz", - "integrity": "sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-Ok9V8o7o6YfSdTTYA/uHH30r3YtOxLD6G3wih/U9DO0ucBBFq8WPt/DslU53OgfteLRHITZny9N/qCUxMf9kjQ==", "cpu": [ "arm64" ], @@ -1344,9 +1236,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.41.tgz", - "integrity": "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-yIsKqMz0CtRnVa6x3Pa+mzTihr4Ty+Z6HfPbZ7RVbk1Uxnco4+CUn7Qbm/5SBol1JD/7nvY8rphAgyAi7Lj6Vg==", "cpu": [ "arm64" ], @@ -1361,9 +1253,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.41.tgz", - "integrity": "sha512-Ho6lIwGJed98zub7n0xcRKuEtnZgbxevAmO4x3zn3C3N4GVXZD5xvCvTVxSMoeBJwTcIYzkVDRTIhylQNsTgLQ==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-GTXe+mxsCGUnJOFMhfGWmefP7Q9TpYUseHvhAhr21nCTgdS8jPsvirb0tJwM3lN0/u/cg7bpFNa16fQrjKrCjQ==", "cpu": [ "x64" ], @@ -1378,9 +1270,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.41.tgz", - "integrity": "sha512-ijAZETywvL+gACjbT4zBnCp5ez1JhTRs6OxRN4J+D6AzDRbU2zb01Esl51RP5/8ZOlvB37xxsRQ3X4YRVyYb3g==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.53.tgz", + "integrity": "sha512-9Tmp7bBvKqyDkMcL4e089pH3RsjD3SUungjmqWtyhNOxoQMh0fSmINTyYV8KXtE+JkxYMPWvnEt+/mfpVCkk8w==", "cpu": [ "x64" ], @@ -1395,9 +1287,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.41.tgz", - "integrity": "sha512-EgIOZt7UildXKFEFvaiLNBXm+4ggQyGe3E5Z1QP9uRcJJs9omihOnm897FwOBQdCuMvI49iBgjFrkhH+wMJ2MA==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.53.tgz", + "integrity": "sha512-a1y5fiB0iovuzdbjUxa7+Zcvgv+mTmlGGC4XydVIsyl48eoxgaYkA3l9079hyTyhECsPq+mbr0gVQsFU11OJAQ==", "cpu": [ "arm" ], @@ -1412,9 +1304,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.41.tgz", - "integrity": "sha512-F8bUwJq8v/JAU8HSwgF4dztoqJ+FjdyjuvX4//3+Fbe2we9UktFeZ27U4lRMXF1vxWtdV4ey6oCSqI7yUrSEeg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-bpIGX+ov9PhJYV+wHNXl9rzq4F0QvILiURn0y0oepbQx+7stmQsKA0DhPGwmhfvF856wq+gbM8L92SAa/CBcLg==", "cpu": [ "arm64" ], @@ -1429,9 +1321,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.41.tgz", - "integrity": "sha512-MioXcCIX/wB1pBnBoJx8q4OGucUAfC1+/X1ilKFsjDK05VwbLZGRgOVD5OJJpUQPK86DhQciNBrfOKDiatxNmg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-bGe5EBB8FVjHBR1mOLOPEFg1Lp3//7geqWkU5NIhxe+yH0W8FVrQ6WRYOap4SUTKdklD/dC4qPLREkMMQ855FA==", "cpu": [ "arm64" ], @@ -1446,9 +1338,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.41.tgz", - "integrity": "sha512-m66M61fizvRCwt5pOEiZQMiwBL9/y0bwU/+Kc4Ce/Pef6YfoEkR28y+DzN9rMdjo8Z28NXjsDPq9nH4mXnAP0g==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.53.tgz", + "integrity": "sha512-qL+63WKVQs1CMvFedlPt0U9PiEKJOAL/bsHMKUDS6Vp2Q+YAv/QLPu8rcvkfIMvQ0FPU2WL0aX4eWwF6e/GAnA==", "cpu": [ "x64" ], @@ -1463,9 +1355,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.41.tgz", - "integrity": "sha512-yRxlSfBvWnnfrdtJfvi9lg8xfG5mPuyoSHm0X01oiE8ArmLRvoJGHUTJydCYz+wbK2esbq5J4B4Tq9WAsOlP1Q==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.53.tgz", + "integrity": "sha512-VGl9JIGjoJh3H8Mb+7xnVqODajBmrdOOb9lxWXdcmxyI+zjB2sux69br0hZJDTyLJfvBoYm439zPACYbCjGRmw==", "cpu": [ "x64" ], @@ -1480,9 +1372,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.41.tgz", - "integrity": "sha512-PHVxYhBpi8UViS3/hcvQQb9RFqCtvFmFU1PvUoTRiUdBtgHA6fONNHU4x796lgzNlVSD3DO/MZNk1s5/ozSMQg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.53.tgz", + "integrity": "sha512-B4iIserJXuSnNzA5xBLFUIjTfhNy7d9sq4FUMQY3GhQWGVhS2RWWzzDnkSU6MUt7/aHUrep0CdQfXUJI9D3W7A==", "cpu": [ "arm64" ], @@ -1497,9 +1389,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.41.tgz", - "integrity": "sha512-OAfcO37ME6GGWmj9qTaDT7jY4rM0T2z0/8ujdQIJQ2x2nl+ztO32EIwURfmXOK0U1tzkyuaKYvE34Pug/ucXlQ==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.53.tgz", + "integrity": "sha512-BUjAEgpABEJXilGq/BPh7jeU3WAJ5o15c1ZEgHaDWSz3LB881LQZnbNJHmUiM4d1JQWMYYyR1Y490IBHi2FPJg==", "cpu": [ "wasm32" ], @@ -1507,16 +1399,16 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.0.5" + "@napi-rs/wasm-runtime": "^1.1.0" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.41.tgz", - "integrity": "sha512-NIYGuCcuXaq5BC4Q3upbiMBvmZsTsEPG9k/8QKQdmrch+ocSy5Jv9tdpdmXJyighKqm182nh/zBt+tSJkYoNlg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-s27uU7tpCWSjHBnxyVXHt3rMrQdJq5MHNv3BzsewCIroIw3DJFjMH1dzCPPMUFxnh1r52Nf9IJ/eWp6LDoyGcw==", "cpu": [ "arm64" ], @@ -1530,27 +1422,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-win32-ia32-msvc": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.41.tgz", - "integrity": "sha512-kANdsDbE5FkEOb5NrCGBJBCaZ2Sabp3D7d4PRqMYJqyLljwh9mDyYyYSv5+QNvdAmifj+f3lviNEUUuUZPEFPw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.41.tgz", - "integrity": "sha512-UlpxKmFdik0Y2VjZrgUCgoYArZJiZllXgIipdBRV1hw6uK45UbQabSTW6Kp6enuOu7vouYWftwhuxfpE8J2JAg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.53.tgz", + "integrity": "sha512-cjWL/USPJ1g0en2htb4ssMjIycc36RvdQAx1WlXnS6DpULswiUTVXPDesTifSKYSyvx24E0YqQkEm0K/M2Z/AA==", "cpu": [ "x64" ], @@ -1565,9 +1440,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.43", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", - "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", "dev": true, "license": "MIT" }, @@ -1642,9 +1517,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.9.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", - "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "peer": true, @@ -1665,19 +1540,19 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", "peer": true, "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", - "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1694,17 +1569,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", - "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/type-utils": "8.46.2", - "@typescript-eslint/utils": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/type-utils": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -1718,7 +1593,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.2", + "@typescript-eslint/parser": "^8.48.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -1734,17 +1609,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", - "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "engines": { @@ -1760,14 +1635,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", - "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.2", - "@typescript-eslint/types": "^8.46.2", + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", "debug": "^4.3.4" }, "engines": { @@ -1782,14 +1657,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", - "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2" + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1800,9 +1675,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", - "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", "dev": true, "license": "MIT", "engines": { @@ -1817,15 +1692,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", - "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1842,9 +1717,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", - "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", "dev": true, "license": "MIT", "engines": { @@ -1856,21 +1731,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", - "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.2", - "@typescript-eslint/tsconfig-utils": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/visitor-keys": "8.46.2", + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -1924,16 +1798,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", - "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.2", - "@typescript-eslint/types": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2" + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1948,13 +1822,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", - "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/types": "8.48.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1966,16 +1840,16 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", - "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.4", + "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.43", + "@rolldown/pluginutils": "1.0.0-beta.47", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, @@ -2043,16 +1917,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/ansis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", - "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2103,19 +1967,6 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { "version": "4.27.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", @@ -2303,15 +2154,15 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/date-and-time": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-4.1.0.tgz", - "integrity": "sha512-tFdrmBPZrR7bun6jqmlEy/dsjV2JLeUdGALfbKdB7mf0ItMNkYYklxjFE0voGg5oapIaE7WctMClkuRzyU9pig==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-4.1.1.tgz", + "integrity": "sha512-IqEzQvjaRO6KL6wxHaBsVZhzym+Kuk3VWmgydIjzbJD0Zwwa1YpvSZ/2rYR5qmAx1aDM8dMAxpQ9Wm4xQoUv3g==", "license": "MIT", "engines": { "node": ">=18" @@ -2422,9 +2273,9 @@ } }, "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", "peer": true, @@ -2432,11 +2283,11 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.1", - "@eslint/core": "^0.16.0", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2483,13 +2334,20 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" @@ -2606,36 +2464,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2650,14 +2478,22 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/file-entry-cache": { @@ -2673,19 +2509,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -2784,9 +2607,9 @@ } }, "node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", "engines": { @@ -2825,6 +2648,23 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -2944,16 +2784,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3368,30 +3198,6 @@ "yallist": "^3.0.2" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3575,13 +3381,14 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -3662,27 +3469,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -3750,9 +3536,9 @@ } }, "node_modules/react-router": { - "version": "7.9.5", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", - "integrity": "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.0.tgz", + "integrity": "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -3822,27 +3608,15 @@ "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rolldown": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.41.tgz", - "integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.53.tgz", + "integrity": "sha512-Qd9c2p0XKZdgT5AYd+KgAMggJ8ZmCs3JnS9PTMWkyUfteKlfmKtxJbWTHkVakxwXs1Ub7jrRYVeFeF7N0sQxyw==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.93.0", - "@rolldown/pluginutils": "1.0.0-beta.41", - "ansis": "=4.2.0" + "@oxc-project/types": "=0.101.0", + "@rolldown/pluginutils": "1.0.0-beta.53" }, "bin": { "rolldown": "bin/cli.mjs" @@ -3851,53 +3625,28 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-beta.41", - "@rolldown/binding-darwin-arm64": "1.0.0-beta.41", - "@rolldown/binding-darwin-x64": "1.0.0-beta.41", - "@rolldown/binding-freebsd-x64": "1.0.0-beta.41", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.41", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.41", - "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.41", - "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.41", - "@rolldown/binding-linux-x64-musl": "1.0.0-beta.41", - "@rolldown/binding-openharmony-arm64": "1.0.0-beta.41", - "@rolldown/binding-wasm32-wasi": "1.0.0-beta.41", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.41", - "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.41", - "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.41" + "@rolldown/binding-android-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.53", + "@rolldown/binding-darwin-x64": "1.0.0-beta.53", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.53", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.53", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.53", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.53", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.53", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.53", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.53" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.41", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.41.tgz", - "integrity": "sha512-ycMEPrS3StOIeb87BT3/+bu+blEtyvwQ4zmo2IcJQy0Rd1DAAhKksA0iUZ3MYSpJtjlPhg0Eo6mvVS6ggPhRbw==", + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", "dev": true, "license": "MIT" }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -4023,51 +3772,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -4118,16 +3822,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.46.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", - "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", + "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", - "@typescript-eslint/typescript-estree": "8.46.2", - "@typescript-eslint/utils": "8.46.2" + "@typescript-eslint/eslint-plugin": "8.48.1", + "@typescript-eslint/parser": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4200,19 +3904,19 @@ }, "node_modules/vite": { "name": "rolldown-vite", - "version": "7.1.14", - "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.1.14.tgz", - "integrity": "sha512-eSiiRJmovt8qDJkGyZuLnbxAOAdie6NCmmd0NkTC0RJI9duiSBTfr8X2mBYJOUFzxQa2USaHmL99J9uMxkjCyw==", + "version": "7.2.10", + "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.2.10.tgz", + "integrity": "sha512-v2ekZjuVLfumjp1Cr7LSQM1n2oOo3+gMruhOgT0Q4/cQ2J3nkTDLTAWLQQ86UHMbFYyVIN1wGh8BEZbvjkyctg==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@oxc-project/runtime": "0.92.0", + "@oxc-project/runtime": "0.101.0", "fdir": "^6.5.0", - "lightningcss": "^1.30.1", + "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", - "rolldown": "1.0.0-beta.41", + "rolldown": "1.0.0-beta.53", "tinyglobby": "^0.2.15" }, "bin": { @@ -4276,38 +3980,6 @@ } } }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4353,6 +4025,30 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index fb5008d..b6036bd 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -12,14 +12,14 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", - "@fontsource/roboto": "^5.2.8", + "@fontsource/roboto": "^5.2.9", "@mdi/js": "^7.4.47", "@mdi/react": "^1.6.1", - "@mui/icons-material": "^7.3.5", - "@mui/material": "^7.3.5", - "@mui/x-data-grid": "^8.18.0", - "@mui/x-date-pickers": "^8.17.0", - "date-and-time": "^4.1.0", + "@mui/icons-material": "^7.3.6", + "@mui/material": "^7.3.6", + "@mui/x-data-grid": "^8.20.0", + "@mui/x-date-pickers": "^8.19.0", + "date-and-time": "^4.1.1", "dayjs": "^1.11.19", "emoji-picker-react": "^4.16.1", "is-cidr": "^6.0.1", @@ -28,23 +28,23 @@ "react-dom": "^19.1.1", "react-favicon": "^2.0.7", "react-json-view-lite": "^2.5.0", - "react-router": "^7.9.5" + "react-router": "^7.10.0" }, "devDependencies": { - "@eslint/js": "^9.36.0", - "@types/node": "^24.6.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react": "^5.0.4", - "eslint": "^9.36.0", - "eslint-plugin-react-hooks": "^5.2.0", + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.22", - "globals": "^16.4.0", + "globals": "^16.5.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.45.0", - "vite": "npm:rolldown-vite@7.1.14" + "typescript-eslint": "^8.48.1", + "vite": "npm:rolldown-vite@7.2.10" }, "overrides": { - "vite": "npm:rolldown-vite@7.1.14" + "vite": "npm:rolldown-vite@7.2.10" } } diff --git a/matrixgw_frontend/src/routes/APITokensRoute.tsx b/matrixgw_frontend/src/routes/APITokensRoute.tsx index af72f09..d9166e2 100644 --- a/matrixgw_frontend/src/routes/APITokensRoute.tsx +++ b/matrixgw_frontend/src/routes/APITokensRoute.tsx @@ -19,7 +19,7 @@ import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider"; import { time } from "../utils/DateUtils"; export function APITokensRoute(): React.ReactElement { - const count = React.useRef(0); + const [count, setCount] = React.useState(0); const [openCreateTokenDialog, setOpenCreateTokenDialog] = React.useState(false); @@ -34,7 +34,7 @@ export function APITokensRoute(): React.ReactElement { }; const handleRefreshTokensList = () => { - count.current += 1; + setCount((c) => c + 1); setList(undefined); }; @@ -79,7 +79,7 @@ export function APITokensRoute(): React.ReactElement { {/* Tokens list */} React.ReactElement; }): React.ReactElement { - const [state, setState] = useState(State.Loading); - - const counter = useRef(null); + const [state, setState] = React.useState(State.Loading); const load = async () => { try { @@ -32,12 +30,10 @@ export function AsyncWidget(p: { } }; - useEffect(() => { - if (counter.current === p.loadKey) return; - counter.current = p.loadKey; - + React.useEffect(() => { load(); - }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [p.loadKey]); if (state === State.Error) return ( From 3ba6543cb40e220bdd94322f0486ea74533b2d1a Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 16:07:34 +0100 Subject: [PATCH 121/124] Remove @mdi/js as a dependency --- matrixgw_frontend/package-lock.json | 17 ------------- matrixgw_frontend/package.json | 2 -- matrixgw_frontend/src/icons/AppIcon.tsx | 18 +++++++++++++ .../src/icons/message-text-fast.svg | 1 + matrixgw_frontend/src/icons/openid.svg | 1 + .../src/routes/auth/LoginRoute.tsx | 9 ++++--- .../src/widgets/auth/BaseLoginPage.tsx | 11 +++----- .../src/widgets/dashboard/DashboardHeader.tsx | 6 ++--- .../widgets/dashboard/DashboardSidebar.tsx | 14 ++++++----- .../src/widgets/dashboard/ThemeSwitcher.tsx | 25 ++++++++++++------- 10 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 matrixgw_frontend/src/icons/AppIcon.tsx create mode 100644 matrixgw_frontend/src/icons/message-text-fast.svg create mode 100644 matrixgw_frontend/src/icons/openid.svg diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json index de031d4..77ae474 100644 --- a/matrixgw_frontend/package-lock.json +++ b/matrixgw_frontend/package-lock.json @@ -11,8 +11,6 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@fontsource/roboto": "^5.2.9", - "@mdi/js": "^7.4.47", - "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.3.6", "@mui/material": "^7.3.6", "@mui/x-data-grid": "^8.20.0", @@ -777,21 +775,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mdi/js": { - "version": "7.4.47", - "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", - "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", - "license": "Apache-2.0" - }, - "node_modules/@mdi/react": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.6.1.tgz", - "integrity": "sha512-4qZeDcluDFGFTWkHs86VOlHkm6gnKaMql13/gpIcUQ8kzxHgpj31NuCkD8abECVfbULJ3shc7Yt4HJ6Wu6SN4w==", - "license": "MIT", - "dependencies": { - "prop-types": "^15.7.2" - } - }, "node_modules/@mui/core-downloads-tracker": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.6.tgz", diff --git a/matrixgw_frontend/package.json b/matrixgw_frontend/package.json index b6036bd..cf6de10 100644 --- a/matrixgw_frontend/package.json +++ b/matrixgw_frontend/package.json @@ -13,8 +13,6 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@fontsource/roboto": "^5.2.9", - "@mdi/js": "^7.4.47", - "@mdi/react": "^1.6.1", "@mui/icons-material": "^7.3.6", "@mui/material": "^7.3.6", "@mui/x-data-grid": "^8.20.0", diff --git a/matrixgw_frontend/src/icons/AppIcon.tsx b/matrixgw_frontend/src/icons/AppIcon.tsx new file mode 100644 index 0000000..bd44d76 --- /dev/null +++ b/matrixgw_frontend/src/icons/AppIcon.tsx @@ -0,0 +1,18 @@ +import { Icon } from "@mui/material"; +import { useActualColorMode } from "../widgets/dashboard/ThemeSwitcher"; + +export function AppIcon(p: { src: string; size?: string }): React.ReactElement { + const { mode } = useActualColorMode(); + return ( + + + + ); +} diff --git a/matrixgw_frontend/src/icons/message-text-fast.svg b/matrixgw_frontend/src/icons/message-text-fast.svg new file mode 100644 index 0000000..d17eeed --- /dev/null +++ b/matrixgw_frontend/src/icons/message-text-fast.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/matrixgw_frontend/src/icons/openid.svg b/matrixgw_frontend/src/icons/openid.svg new file mode 100644 index 0000000..a1a671a --- /dev/null +++ b/matrixgw_frontend/src/icons/openid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/matrixgw_frontend/src/routes/auth/LoginRoute.tsx b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx index 75b9457..7ffe589 100644 --- a/matrixgw_frontend/src/routes/auth/LoginRoute.tsx +++ b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx @@ -1,9 +1,10 @@ -import { Alert, Box, Button, CircularProgress } from "@mui/material"; -import Icon from "@mdi/react"; -import { mdiOpenid } from "@mdi/js"; +import { Alert, Box, Button, CircularProgress, SvgIcon } from "@mui/material"; import { ServerApi } from "../../api/ServerApi"; import React from "react"; import { AuthApi } from "../../api/AuthApi"; +import openid from "../../icons/openid.svg"; +import { createSvgIcon } from "@mui/x-data-grid/internals"; +import { AppIcon } from "../../icons/AppIcon"; export function LoginRoute(): React.ReactElement { const [loading, setLoading] = React.useState(false); @@ -40,7 +41,7 @@ export function LoginRoute(): React.ReactElement { fullWidth variant="outlined" onClick={authWithOpenID} - startIcon={} + startIcon={} > Sign in with {ServerApi.Config.oidc_provider_name} diff --git a/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx b/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx index a6d86da..4c34f6f 100644 --- a/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx +++ b/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx @@ -1,10 +1,10 @@ -import { mdiMessageTextFast } from "@mdi/js"; -import Icon from "@mdi/react"; import { Typography } from "@mui/material"; import MuiCard from "@mui/material/Card"; import Stack from "@mui/material/Stack"; import { styled } from "@mui/material/styles"; import { Outlet } from "react-router"; +import { AppIcon } from "../../icons/AppIcon"; +import mdiMessageTextFast from "../../icons/message-text-fast.svg"; const Card = styled(MuiCard)(({ theme }) => ({ display: "flex", @@ -57,12 +57,7 @@ export function BaseLoginPage(): React.ReactElement { variant="h4" sx={{ width: "100%", fontSize: "clamp(2rem, 10vw, 2.15rem)" }} > - {" "} - MatrixGW + MatrixGW diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx index 295991f..f492e76 100644 --- a/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx @@ -1,5 +1,3 @@ -import { mdiMessageTextFast } from "@mdi/js"; -import Icon from "@mdi/react"; import LogoutIcon from "@mui/icons-material/Logout"; import MenuIcon from "@mui/icons-material/Menu"; import MenuOpenIcon from "@mui/icons-material/MenuOpen"; @@ -13,6 +11,8 @@ import Toolbar from "@mui/material/Toolbar"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; import * as React from "react"; +import { AppIcon } from "../../icons/AppIcon"; +import mdiMessageTextFast from "../../icons/message-text-fast.svg"; import { RouterLink } from "../RouterLink"; import { useUserInfo } from "./BaseAuthenticatedPage"; import ThemeSwitcher from "./ThemeSwitcher"; @@ -101,7 +101,7 @@ export default function DashboardHeader({ - + } + icon={} href="/" mini={viewport === "desktop"} /> } + icon={} href="/matrix_link" mini={viewport === "desktop"} /> } + icon={} href="/tokens" mini={viewport === "desktop"} /> } + icon={} href="/wsdebug" mini={viewport === "desktop"} /> diff --git a/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx b/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx index 7c22dc3..aa2d29d 100644 --- a/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx +++ b/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx @@ -7,9 +7,10 @@ import DarkModeIcon from "@mui/icons-material/DarkMode"; import LightModeIcon from "@mui/icons-material/LightMode"; import type {} from "@mui/material/themeCssVarsAugmentation"; -export default function ThemeSwitcher() { - const theme = useTheme(); - +export function useActualColorMode(): { + mode: "light" | "dark"; + setMode: (mode: "light" | "dark") => void; +} { const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); const preferredMode = prefersDarkMode ? "dark" : "light"; @@ -17,21 +18,27 @@ export default function ThemeSwitcher() { const paletteMode = !mode || mode === "system" ? preferredMode : mode; + return { mode: paletteMode, setMode }; +} + +export default function ThemeSwitcher() { + const theme = useTheme(); + + const { mode, setMode } = useActualColorMode(); + const toggleMode = React.useCallback(() => { - setMode(paletteMode === "dark" ? "light" : "dark"); - }, [setMode, paletteMode]); + setMode(mode === "dark" ? "light" : "dark"); + }, [mode, setMode]); return (
    Date: Wed, 3 Dec 2025 16:10:56 +0100 Subject: [PATCH 122/124] Fix emoji size --- matrixgw_frontend/src/widgets/EmojiIcon.tsx | 11 +++++++++-- .../src/widgets/messages/RoomMessagesList.tsx | 6 ++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/matrixgw_frontend/src/widgets/EmojiIcon.tsx b/matrixgw_frontend/src/widgets/EmojiIcon.tsx index cde45a0..c0a3c64 100644 --- a/matrixgw_frontend/src/widgets/EmojiIcon.tsx +++ b/matrixgw_frontend/src/widgets/EmojiIcon.tsx @@ -16,9 +16,16 @@ function emojiUnicode(emoji: string): string { return s.includes("f") ? s : `${s}-fe0f`; } -export function EmojiIcon(p: { emojiKey: string }): React.ReactElement { +export function EmojiIcon(p: { + emojiKey: string; + size?: number; +}): React.ReactElement { const unified = emojiUnicode(p.emojiKey); return ( - + ); } diff --git a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx index e0e2cf7..3fc340a 100644 --- a/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -465,11 +465,9 @@ function RoomMessage(p: { }} >
    - -
    -
    - {reactions.length} +
    +
    {reactions.length}
    } /> From b47ec37a76bf593342bda29543649e756988dec7 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 16:12:00 +0100 Subject: [PATCH 123/124] Remove unused imports in login route --- matrixgw_frontend/src/routes/auth/LoginRoute.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/matrixgw_frontend/src/routes/auth/LoginRoute.tsx b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx index 7ffe589..e6f476e 100644 --- a/matrixgw_frontend/src/routes/auth/LoginRoute.tsx +++ b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx @@ -1,10 +1,9 @@ -import { Alert, Box, Button, CircularProgress, SvgIcon } from "@mui/material"; -import { ServerApi } from "../../api/ServerApi"; +import { Alert, Box, Button, CircularProgress } from "@mui/material"; import React from "react"; import { AuthApi } from "../../api/AuthApi"; -import openid from "../../icons/openid.svg"; -import { createSvgIcon } from "@mui/x-data-grid/internals"; +import { ServerApi } from "../../api/ServerApi"; import { AppIcon } from "../../icons/AppIcon"; +import openid from "../../icons/openid.svg"; export function LoginRoute(): React.ReactElement { const [loading, setLoading] = React.useState(false); From fe9c692e127a75df098640f9614ee0404c5a526d Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 3 Dec 2025 16:18:03 +0100 Subject: [PATCH 124/124] Fix alignment inside WSDebugRoute --- matrixgw_frontend/src/routes/WSDebugRoute.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/matrixgw_frontend/src/routes/WSDebugRoute.tsx b/matrixgw_frontend/src/routes/WSDebugRoute.tsx index b572c3a..56cd9e9 100644 --- a/matrixgw_frontend/src/routes/WSDebugRoute.tsx +++ b/matrixgw_frontend/src/routes/WSDebugRoute.tsx @@ -23,13 +23,21 @@ export function WSDebugRoute(): React.ReactElement { return ( -
    - State:{" "} - + {/* Status bar */} +
    + State: + {state}
    + + {/* WS messages list */} {messages.map((msg, id) => (