diff --git a/.drone.yml b/.drone.yml index 0b0b2ce..bdbfb96 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,57 +4,101 @@ type: docker name: default steps: -# Code quality -- name: code_quality - image: rust - volumes: - - name: rust_registry - path: /usr/local/cargo/registry - commands: - - rustup component add clippy - - cargo clippy -- -D warnings - - cargo test + # Frontend + - name: web_build + image: node:23 + volumes: + - name: web_app + path: /tmp/web_build + commands: + - node -v + - npm -v + - cd matrixgw_frontend + - npm install + - npm run lint + - npm run build + - mv dist /tmp/web_build -# Build source code -- name: compile - image: rust - depends_on: - - code_quality - when: - event: - - tag - volumes: - - name: rust_registry - path: /usr/local/cargo/registry - - name: releases - path: /tmp/releases - commands: - - cargo build --release - - ls -lah target/release/matrix_gateway - - cp target/release/matrix_gateway /tmp/releases + # Backend + - name: backend_fetch_deps + image: rust + volumes: + - name: rust_registry + path: /usr/local/cargo/registry + commands: + - cd matrixgw_backend + - cargo fetch -# Auto-release to Gitea -- name: gitea_release - image: plugins/gitea-release - depends_on: - - compile - when: - event: - - tag - volumes: - - name: releases - path: /tmp/releases - environment: - PLUGIN_API_KEY: - from_secret: GITEA_API_KEY # needs permission write:repository - settings: - base_url: https://gitea.communiquons.org - files: - - /tmp/releases/matrix_gateway - checksum: sha512 + - 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: releases - temp: {} \ No newline at end of file + temp: {} + - name: web_app + temp: {} + - name: release + temp: {} diff --git a/Makefile b/Makefile index 2834da9..ad8433a 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,18 @@ DOCKER_TEMP_DIR=temp -all: gateway +all: frontend backend -gateway: - cargo clippy -- -D warnings && cargo build --release +frontend: + cd matrixgw_frontend && npm run build && cd .. + rm -rf matrixgw_backend/static + mv matrixgw_frontend/dist matrixgw_backend/static -gateway_docker: gateway +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 target/release/matrix_gateway $(DOCKER_TEMP_DIR) - docker build -t pierre42100/matrix_gateway -f ./Dockerfile "$(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/README.md b/README.md index 62e7b9b..25667cd 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,8 +16,13 @@ docker run --rm -it docker.io/pierre42100/matrix_gateway --help ``` ## Setup dev environment + +### Dependencies ``` -mkdir -p storage/maspostgres storage/synapse storage/minio +sudo apt install -y libsqlite3-dev + +cd matrixgw_backend +mkdir -p storage/maspostgres storage/synapse docker compose up ``` @@ -33,12 +37,22 @@ 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 +cd matrixgw_backend +cargo fmt && cargo clippy && cargo run -- +``` + +### Frontend +```bash +cd matrixgw_frontend +npm install +npm run dev +``` 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/.gitignore b/matrixgw_backend/.gitignore similarity index 53% rename from .gitignore rename to matrixgw_backend/.gitignore index 79e4ba8..afb9e89 100644 --- a/.gitignore +++ b/matrixgw_backend/.gitignore @@ -1,3 +1,5 @@ storage +app_storage .idea target +static \ No newline at end of file diff --git a/Cargo.lock b/matrixgw_backend/Cargo.lock similarity index 56% rename from Cargo.lock rename to matrixgw_backend/Cargo.lock index 9bde28a..6b2ea6b 100644 --- a/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" @@ -19,6 +31,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" @@ -96,9 +123,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" dependencies = [ "futures-core", "tokio", @@ -116,7 +143,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "socket2 0.5.8", + "socket2 0.5.10", "tokio", "tracing", ] @@ -196,7 +223,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.0", + "socket2 0.6.1", "time", "tracing", "url", @@ -230,9 +257,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aead" @@ -240,7 +267,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common 0.1.6", + "crypto-common", "generic-array", ] @@ -271,9 +298,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -293,6 +320,12 @@ 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" @@ -304,9 +337,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -319,37 +352,37 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] @@ -358,12 +391,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" @@ -375,54 +434,21 @@ 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.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38fa22307249f86fb7fad906fcae77f2564caeb56d7209103c551cd1cf4798f" +checksum = "9dbc3a507a82b17ba0d98f6ce8fd6954ea0c8152e98009d36a40d8dcc8ce078a" [[package]] -name = "askama" -version = "0.14.0" +name = "ascii_utils" +version = "0.9.3" 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", -] +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" [[package]] name = "assign" @@ -430,6 +456,31 @@ 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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -454,9 +505,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.87" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -469,60 +520,21 @@ 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.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[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", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backon" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" dependencies = [ "fastrand", + "gloo-timers", + "tokio", ] [[package]] @@ -551,18 +563,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.7.1" +version = "1.8.0" 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", -] +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "binstring" @@ -572,9 +575,27 @@ checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +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" @@ -587,6 +608,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" @@ -597,19 +631,67 @@ dependencies = [ ] [[package]] -name = "block-buffer" -version = "0.11.0" +name = "block-padding" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "hybrid-array", + "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.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9991eea70ea4f293524138648e41ee89b0b2b12ddef3b255effa43c8056e0e0d" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -627,10 +709,19 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.17.0" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +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" @@ -645,39 +736,87 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] -name = "bytestring" -version = "1.4.0" +name = "bytesize" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + +[[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.3" +name = "cbc" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "rustversion", + "cipher", ] [[package]] name = "cc" -version = "1.2.16" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", ] [[package]] -name = "cfg-if" -version = "1.0.0" +name = "census" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +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" +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" @@ -688,6 +827,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link 0.2.1", ] @@ -698,8 +838,9 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.6", + "crypto-common", "inout", + "zeroize", ] [[package]] @@ -738,9 +879,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "coarsetime" @@ -755,9 +896,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -774,16 +915,29 @@ dependencies = [ ] [[package]] -name = "compact_str" -version = "0.7.1" +name = "compression-codecs" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad" dependencies = [ - "castaway", - "cfg-if", - "itoa", - "ryu", - "static_assertions", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", ] [[package]] @@ -792,37 +946,14 @@ 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" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2459fc9262a1aa204eb4b5764ad4f189caec88aea9634389c0a25f8be7f6265e" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] [[package]] name = "constant_time_eq" @@ -830,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" @@ -842,7 +982,7 @@ dependencies = [ "hmac", "percent-encoding", "rand 0.8.5", - "sha2 0.10.8", + "sha2", "subtle", "time", "version_check", @@ -875,18 +1015,52 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] -name = "crunchy" -version = "0.2.3" +name = "crossbeam-channel" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +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" @@ -902,24 +1076,15 @@ 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", "typenum", ] -[[package]] -name = "crypto-common" -version = "0.2.0-rc.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919bd05924682a5480aec713596b9e2aabed3a0a6022fab6847f85a99e5f190a" -dependencies = [ - "hybrid-array", -] - [[package]] name = "ct-codecs" version = "1.1.6" @@ -935,6 +1100,118 @@ 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" @@ -942,43 +1219,108 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c03c416ed1a30fbb027ef484ba6ab6f80e1eada675e1a2b92fd673c045a1f1d" [[package]] -name = "der" -version = "0.7.9" +name = "deadpool" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" dependencies = [ - "const-oid 0.9.6", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", "pem-rfc7468", "zeroize", ] [[package]] name = "deranged" -version = "0.3.11" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[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", ] @@ -989,23 +1331,12 @@ 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", + "block-buffer", + "const-oid", + "crypto-common", "subtle", ] -[[package]] -name = "digest" -version = "0.11.0-rc.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea390c940e465846d64775e55e3115d5dc934acb953de6f6e6360bc232fe2bf7" -dependencies = [ - "block-buffer 0.11.0", - "const-oid 0.10.0", - "crypto-common 0.2.0-rc.5", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -1018,13 +1349,10 @@ dependencies = [ ] [[package]] -name = "dlv-list" -version = "0.5.2" +name = "downcast-rs" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "ecdsa" @@ -1033,7 +1361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", - "digest 0.10.7", + "digest", "elliptic-curve", "rfc6979", "signature", @@ -1041,15 +1369,47 @@ dependencies = [ ] [[package]] -name = "ed25519-compact" -version = "2.1.1" +name = "ed25519" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-compact" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ce99a9e19c84beb4cc35ece85374335ccc398240712114c85038319ed709bd" dependencies = [ "ct-codecs", - "getrandom 0.2.15", + "getrandom 0.3.4", ] +[[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" @@ -1058,7 +1418,7 @@ checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct 0.2.0", "crypto-bigint", - "digest 0.10.7", + "digest", "ff", "generic-array", "group", @@ -1082,9 +1442,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -1111,14 +1471,100 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +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" @@ -1136,10 +1582,22 @@ dependencies = [ ] [[package]] -name = "flate2" -version = "1.1.0" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -1153,9 +1611,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "foreign-types" @@ -1174,13 +1632,48 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 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" @@ -1196,6 +1689,17 @@ 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" @@ -1231,6 +1735,7 @@ 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", @@ -1255,27 +1760,29 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.13.3+wasi-0.2.2", - "windows-targets 0.52.6", + "r-efi", + "wasip2", + "wasm-bindgen", ] [[package]] @@ -1288,6 +1795,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" @@ -1299,6 +1831,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" @@ -1320,16 +1864,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.8" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http 1.4.0", "indexmap", "slab", "tokio", @@ -1345,9 +1889,53 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[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.4.0", + "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.4.0", +] [[package]] name = "heck" @@ -1355,6 +1943,12 @@ 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" @@ -1376,7 +1970,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -1391,7 +1985,7 @@ version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -1400,18 +1994,27 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] -name = "home" -version = "0.5.11" +name = "html5ever" +version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ - "windows-sys 0.59.0", + "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" @@ -1425,15 +2028,23 @@ 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", ] +[[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" @@ -1441,7 +2052,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.3.1", + "http 1.4.0", ] [[package]] @@ -1452,7 +2063,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.3.1", + "http 1.4.0", "http-body", "pin-project-lite", ] @@ -1469,30 +2080,23 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hybrid-array" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" -dependencies = [ - "typenum", -] - [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", - "h2 0.4.8", - "http 1.3.1", + "futures-core", + "h2 0.4.12", + "http 1.4.0", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1500,12 +2104,11 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", - "http 1.3.1", + "http 1.4.0", "hyper", "hyper-util", "rustls", @@ -1533,33 +2136,50 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +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", + "libc", + "percent-encoding", "pin-project-lite", - "socket2 0.5.8", + "socket2 0.6.1", + "system-configuration", "tokio", "tower-service", "tracing", + "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.61" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core 0.52.0", ] @@ -1575,21 +2195,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1598,104 +2219,72 @@ dependencies = [ "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" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 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" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", + "icu_locale_core", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] [[package]] -name = "icu_provider_macros" -version = "1.5.0" +name = "ident_case" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1704,14 +2293,38 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "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" @@ -1719,14 +2332,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" [[package]] -name = "indexmap" -version = "2.8.0" +name = "include_dir" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.1", "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", ] [[package]] @@ -1735,6 +2394,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding", "generic-array", ] @@ -1748,10 +2408,38 @@ dependencies = [ ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.1" +name = "iri-string" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +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 = "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" @@ -1761,22 +2449,22 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.4" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.4" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" dependencies = [ "proc-macro2", "quote", @@ -1785,18 +2473,19 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -1842,7 +2531,7 @@ dependencies = [ "serde", "serde_json", "superboring", - "thiserror", + "thiserror 2.0.17", "zeroize", ] @@ -1856,7 +2545,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "sha2 0.10.8", + "sha2", "signature", ] @@ -1886,6 +2575,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" @@ -1896,16 +2608,32 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.174" +name = "levenshtein_automata" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +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" @@ -1923,15 +2651,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "local-channel" @@ -1952,19 +2680,96 @@ checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[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" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abad4bc63045f04cfc55aa4c55d4ec0a890c377ce56463bfc2adc2bc059c4b84" +dependencies = [ + "fast_chemail", + "once_cell", +] [[package]] name = "maplit" @@ -1973,45 +2778,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] -name = "matrix_gateway" -version = "0.1.0" +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" 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.3", - "thiserror", - "tokio", - "urlencoding", - "uuid", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", ] [[package]] -name = "maybe-async" -version = "0.2.10" +name = "match_token" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", @@ -2019,16 +2803,327 @@ dependencies = [ ] [[package]] -name = "md5" -version = "0.8.0" +name = "matrix-pickle" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" +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.4.0", + "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" +dependencies = [ + "actix-cors", + "actix-remote-ip", + "actix-session", + "actix-web", + "actix-ws", + "anyhow", + "base16ct 0.3.0", + "bytes", + "clap", + "env_logger", + "futures-util", + "hex", + "infer", + "ipnet", + "jwt-simple", + "lazy-regex", + "lazy_static", + "light-openid", + "log", + "mailchecker", + "matrix-sdk", + "mime_guess", + "ractor", + "rand 0.9.2", + "rust-embed", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.17", + "tokio", + "url", + "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.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +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" @@ -2036,6 +3131,12 @@ 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 = "mime_guess" version = "2.0.5" @@ -2047,35 +3148,39 @@ dependencies = [ ] [[package]] -name = "minidom" -version = "0.16.0" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e394a0e3c7ccc2daea3dffabe82f09857b6b510cb25af87d54bf3e910ac1642d" -dependencies = [ - "rxml", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.5" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi", + "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" @@ -2094,12 +3199,28 @@ dependencies = [ ] [[package]] -name = "ntapi" -version = "0.4.1" +name = "new_debug_unreachable" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "winapi", + "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]] @@ -2114,11 +3235,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -2166,29 +3286,52 @@ dependencies = [ ] [[package]] -name = "objc2-core-foundation" -version = "0.3.2" +name = "num_cpus" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "bitflags", + "hermit-abi", + "libc", ] [[package]] -name = "objc2-io-kit" -version = "0.3.2" +name = "oauth2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "libc", - "objc2-core-foundation", + "base64 0.22.1", + "chrono", + "getrandom 0.2.16", + "http 1.4.0", + "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.0" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" +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 = "oneshot" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" [[package]] name = "opaque-debug" @@ -2198,9 +3341,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", "cfg-if", @@ -2230,9 +3373,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -2241,13 +3384,12 @@ dependencies = [ ] [[package]] -name = "ordered-multimap" -version = "0.7.3" +name = "ownedbytes" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" dependencies = [ - "dlv-list", - "hashbrown 0.14.5", + "stable_deref_trait", ] [[package]] @@ -2259,7 +3401,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.8", + "sha2", ] [[package]] @@ -2271,14 +3413,20 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", - "sha2 0.10.8", + "sha2", ] [[package]] -name = "parking_lot" -version = "0.12.3" +name = "parking" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[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", @@ -2286,15 +3434,31 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", +] + +[[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]] @@ -2308,9 +3472,47 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +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" @@ -2351,6 +3553,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" @@ -2365,9 +3578,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -2378,6 +3591,15 @@ 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" @@ -2393,6 +3615,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" @@ -2404,41 +3642,101 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "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.94" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] -name = "quick-xml" -version = "0.38.4" +name = "prost" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ - "memchr", - "serde", + "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.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 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 = "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" @@ -2486,7 +3784,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] @@ -2495,14 +3793,68 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.1", + "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.5" +version = "0.32.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" +checksum = "014cc767fefab6a3e798ca45112bccad9c6e0e218fbd49720042716c73cfef44" dependencies = [ "arc-swap", "backon", @@ -2516,7 +3868,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "ryu", - "socket2 0.6.0", + "socket2 0.6.1", "tokio", "tokio-util", "url", @@ -2524,18 +3876,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -2545,9 +3897,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -2556,60 +3908,58 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.14" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e327e510263980e231de548a33e63d34962d29ae61b467389a1a09627a254" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ + "async-compression", "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.8", - "http 1.3.1", + "h2 0.4.12", + "http 1.4.0", "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", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tokio-util", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "windows-registry", ] [[package]] @@ -2630,27 +3980,49 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] -name = "rsa" -version = "0.9.8" +name = "rmp" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" dependencies = [ - "const-oid 0.9.6", - "digest 0.10.7", + "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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", "rand_core 0.6.4", - "sha2 0.10.8", + "sha2", "signature", "spki", "subtle", @@ -2659,53 +4031,33 @@ dependencies = [ [[package]] name = "ruma" -version = "0.12.6" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3714d4ebd4314e6510bc64194fcdea1b51fe47898169a08f1bb4912e5c10e2c5" +checksum = "c7b698b728bc3747f564a9115c83b4f2e229b52377f6a1cca2e6add9cf4a13be" dependencies = [ "assign", "js_int", "js_option", - "ruma-client", "ruma-client-api", "ruma-common", "ruma-events", + "ruma-federation-api", + "ruma-html", + "ruma-signatures", "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" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9e9c613cfda4923b851c5d8bc442305905bee4f0c2b924564b00e71636c8d4" +checksum = "b54e56c591f9ad686defb0bacbebba5c8882eb0c9f8734f6a080345b4e3dd941" dependencies = [ "as_variant", "assign", "bytes", "date_header", - "http 1.3.1", + "http 1.4.0", "js_int", "js_option", "maplit", @@ -2714,24 +4066,25 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror", + "thiserror 2.0.17", "url", "web-time", ] [[package]] name = "ruma-common" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387e1898e868d32ff7b205e7db327361d5dcf635c00a8ae5865068607595a9cf" +checksum = "ac7f59b9f7639667d0d6ae3ae242c8912e9ed061cea1fbaf72710a402e83b53e" dependencies = [ "as_variant", "base64 0.22.1", "bytes", "form_urlencoded", - "getrandom 0.2.15", - "http 1.3.1", + "getrandom 0.2.16", + "http 1.4.0", "indexmap", + "js-sys", "js_int", "konst", "percent-encoding", @@ -2742,20 +4095,21 @@ dependencies = [ "serde", "serde_html_form", "serde_json", - "thiserror", + "thiserror 2.0.17", "time", "tracing", "url", "uuid", "web-time", "wildmatch", + "zeroize", ] [[package]] name = "ruma-events" -version = "0.30.5" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f141b37dcd3cfa1199d6a13929db59be529b2c69107edc9f1702b81015e970b2" +checksum = "34fa815769ed4fe1ef5b50aa0ba6f350317c13b5a9f1e008b014f4a3ddf14204" dependencies = [ "as_variant", "indexmap", @@ -2768,28 +4122,62 @@ dependencies = [ "ruma-macros", "serde", "serde_json", - "thiserror", + "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.4.0", + "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.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad674b5e5368c53a2c90fde7dac7e30747004aaf7b1827b72874a25fc06d4d8" +checksum = "14a7b93ac1e571c585f8fa5cef09c07bb8a15529775fd56b9a3eac4f9233dff2" dependencies = [ "js_int", - "thiserror", + "thiserror 2.0.17", ] [[package]] name = "ruma-macros" -version = "0.15.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ff13fbd6045a7278533390826de316d6116d8582ed828352661337b0c422e1c" +checksum = "0c9911c7188517f28505d2d513339511d00e0f50cec5c2dde820cd0ec7e6a833" dependencies = [ "cfg-if", "proc-macro-crate", @@ -2801,6 +4189,36 @@ dependencies = [ "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-embed" version = "8.9.0" @@ -2831,54 +4249,18 @@ version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" dependencies = [ - "sha2 0.10.8", + "sha2", "walkdir", ] [[package]] -name = "rust-ini" -version = "0.21.1" +name = "rust-stemmers" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" dependencies = [ - "cfg-if", - "ordered-multimap", - "trim-in-place", -] - -[[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 0.10.8", - "sysinfo", - "thiserror", - "time", - "tokio", - "tokio-stream", - "url", ] [[package]] @@ -2888,23 +4270,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] -name = "rustix" -version = "1.0.2" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[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.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.23" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "rustls-pki-types", @@ -2914,25 +4305,19 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ - "rustls-pki-types", + "zeroize", ] -[[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" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -2941,28 +4326,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" 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", -] +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2981,11 +4347,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3023,14 +4389,20 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "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" @@ -3041,6 +4413,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" @@ -3063,15 +4456,15 @@ dependencies = [ [[package]] name = "serde_html_form" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", "indexmap", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] @@ -3088,10 +4481,21 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "0.6.8" +name = "serde_path_to_error" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +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", ] @@ -3116,29 +4520,27 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] -name = "sha2" -version = "0.11.0-rc.3" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d43dc0354d88b791216bb5c1bfbb60c0814460cc653ae0ebd71f286d0bd927" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.11.0-rc.4", + "lazy_static", ] [[package]] @@ -3149,9 +4551,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -3162,30 +4564,48 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest 0.10.7", + "digest", "rand_core 0.6.4", ] [[package]] -name = "slab" -version = "0.4.9" +name = "simd-adler32" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +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 = [ - "autocfg", + "serde", ] [[package]] -name = "smallvec" -version = "1.14.0" +name = "slab" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +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.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3193,12 +4613,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3219,15 +4639,34 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "static_assertions" -version = "1.1.0" +name = "string_cache" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +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" @@ -3235,6 +4674,28 @@ 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" @@ -3247,7 +4708,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", "hmac-sha256", "hmac-sha512", "rand 0.8.5", @@ -3256,9 +4717,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -3276,9 +4737,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -3321,17 +4782,173 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.18.0" +name = "tantivy" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.3.1", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "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]] @@ -3340,7 +4957,27 @@ version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "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 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -3355,10 +4992,19 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.39" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -3371,39 +5017,45 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.20" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +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.7.6" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "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" @@ -3416,8 +5068,9 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.0", + "socket2 0.6.1", "tokio-macros", + "tracing", "windows-sys 0.61.2", ] @@ -3444,9 +5097,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -3461,13 +5114,14 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -3477,36 +5131,90 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.8.20" +name = "tokio_with_wasm" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +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", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] -name = "toml_edit" -version = "0.22.24" +name = "toml_datetime" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +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", + "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", ] @@ -3525,6 +5233,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.4.0", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -3539,9 +5265,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", @@ -3551,9 +5277,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3562,18 +5288,38 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", + "valuable", ] [[package]] -name = "trim-in-place" -version = "0.1.7" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] [[package]] name = "try-lock" @@ -3583,15 +5329,15 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "typewit" -version = "1.11.0" +version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb77c29baba9e4d3a6182d51fa75e3215c7fd1dab8f4ea9d107c716878e55fc0" +checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" dependencies = [ "typewit_proc_macros", ] @@ -3602,6 +5348,16 @@ 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 = "unicase" version = "2.8.1" @@ -3610,9 +5366,24 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +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-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-xid" @@ -3626,7 +5397,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common 0.1.6", + "crypto-common", "subtle", ] @@ -3638,9 +5409,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -3655,10 +5426,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" [[package]] -name = "utf16_iter" +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 = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" [[package]] name = "utf8_iter" @@ -3674,16 +5451,22 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom 0.3.1", + "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "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" @@ -3696,6 +5479,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 = "walkdir" version = "2.5.0" @@ -3717,17 +5530,17 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -3736,40 +5549,27 @@ version = "0.12.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" dependencies = [ - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" 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" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -3780,9 +5580,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3790,22 +5590,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -3825,9 +5625,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -3845,9 +5645,9 @@ dependencies = [ [[package]] name = "wildmatch" -version = "2.4.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" +checksum = "29333c3ea1ba8b17211763463ff24ee84e41c78224c16b001cd907e663a38c68" [[package]] name = "winapi" @@ -3867,11 +5667,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3880,59 +5680,17 @@ 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 0.61.2", - "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 0.61.2", -] - [[package]] name = "windows-core" -version = "0.52.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.1.3", + "windows-link", "windows-result", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", + "windows-strings", ] [[package]] @@ -3957,53 +5715,37 @@ 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" 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 0.61.2", - "windows-link 0.1.3", -] - [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ + "windows-link", "windows-result", - "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-strings", ] [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link 0.1.3", ] [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link 0.1.3", ] @@ -4035,13 +5777,22 @@ 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", + "windows-link", ] [[package]] @@ -4062,18 +5813,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 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", + "windows-link", + "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]] @@ -4093,9 +5845,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -4105,9 +5857,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -4117,9 +5869,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -4129,9 +5881,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -4141,9 +5893,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -4153,9 +5905,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -4165,9 +5917,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -4177,47 +5929,55 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.4" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.33.0" +name = "wit-bindgen" +version = "0.46.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" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +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.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -4225,9 +5985,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -4237,18 +5997,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -4281,12 +6041,37 @@ 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" +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.10.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -4295,9 +6080,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -4315,18 +6100,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.3" +version = "7.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" +version = "2.0.16+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/matrixgw_backend/Cargo.toml similarity index 55% rename from Cargo.toml rename to matrixgw_backend/Cargo.toml index 89ea383..dedead8 100644 --- a/Cargo.toml +++ b/matrixgw_backend/Cargo.toml @@ -1,35 +1,38 @@ [package] -name = "matrix_gateway" +name = "matrixgw_backend" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] -log = "0.4.28" env_logger = "0.11.8" +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"] } -serde_json = "1.0.145" -rust-s3 = { version = "0.37.0", features = ["tokio"] } +tokio = { version = "1.48.0", features = ["full"] } actix-web = "4.12.1" 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.9.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" +actix-cors = "0.7.1" +light-openid = "1.0.4" bytes = "1.11.0" -sha2 = "0.11.0-rc.3" +sha2 = "0.10.9" base16ct = { version = "0.3.0", features = ["alloc"] } -ruma = { version = "0.12.6", features = ["client-api-c", "client-ext-client-api", "client-hyper-native-tls", "rand"] } +futures-util = "0.3.31" +jwt-simple = { version = "0.12.13", default-features = false, features = ["pure-rust"] } +thiserror = "2.0.17" +uuid = { version = "1.19.0", features = ["v4", "serde"] } +ipnet = { version = "2.11.0", features = ["serde"] } +rand = "0.9.2" +hex = "0.4.3" +mailchecker = "6.0.19" +matrix-sdk = { version = "0.14.0" } +url = "2.5.7" +ractor = "0.15.9" +serde_json = "1.0.145" +lazy-regex = "3.4.2" actix-ws = "0.3.0" -tokio = { version = "1.48.0", features = ["rt", "time", "macros", "rt-multi-thread"] } +infer = "0.19.0" +rust-embed = "8.9.0" +mime_guess = "2.0.5" \ No newline at end of file diff --git a/Dockerfile b/matrixgw_backend/Dockerfile similarity index 100% rename from Dockerfile rename to matrixgw_backend/Dockerfile 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. diff --git a/docker-compose.yml b/matrixgw_backend/docker-compose.yml similarity index 86% rename from docker-compose.yml rename to matrixgw_backend/docker-compose.yml index d37a458..be4a7bb 100644 --- a/docker-compose.yml +++ b/matrixgw_backend/docker-compose.yml @@ -80,36 +80,23 @@ 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 + - "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 - 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 + command: [ "dex", "serve", "/conf/dex.config.yaml" ] redis: 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/docker/dex/dex.config.yaml b/matrixgw_backend/docker/dex/dex.config.yaml similarity index 92% rename from docker/dex/dex.config.yaml rename to matrixgw_backend/docker/dex/dex.config.yaml index 7704800..0b9d381 100644 --- a/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/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/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/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 74% rename from examples/api_curl.rs rename to matrixgw_backend/examples/api_curl.rs index a2669fd..f776dc8 100644 --- a/examples/api_curl.rs +++ b/matrixgw_backend/examples/api_curl.rs @@ -1,11 +1,14 @@ 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::{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)] @@ -19,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)] @@ -59,7 +62,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, @@ -68,17 +71,20 @@ 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!"); 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 new file mode 100644 index 0000000..87c3b2c --- /dev/null +++ b/matrixgw_backend/src/app_config.rs @@ -0,0 +1,248 @@ +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)] +#[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:5173")] + pub website_origin: String, + + /// Proxy IP, might end with a star "*" + #[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, + + /// 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 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, + + /// 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, + + /// 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, +} + +lazy_static::lazy_static! { + static ref ARGS: AppConfig = { + AppConfig::parse() + }; +} + +impl AppConfig { + /// Get parsed command line arguments + pub fn get() -> &'static AppConfig { + &ARGS + } + + /// Get auto login email (if not empty) + pub fn unsecure_auto_login_email(&self) -> Option { + match self.unsecure_auto_login_email.as_deref() { + None | Some("") => None, + Some(s) => Some(UserEmail(s.to_owned())), + } + } + + /// 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 + } + + /// 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!( + "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(), + name: self.oidc_provider_name.as_str(), + redirect_url: self + .oidc_redirect_url + .replace("APP_ORIGIN", &self.website_origin), + } + } + + /// 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()) + } + + /// 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()) + } + + /// 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") + } + + /// 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)] +pub struct OIDCProvider<'a> { + pub name: &'a str, + 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/broadcast_messages.rs b/matrixgw_backend/src/broadcast_messages.rs new file mode 100644 index 0000000..a398387 --- /dev/null +++ b/matrixgw_backend/src/broadcast_messages.rs @@ -0,0 +1,43 @@ +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::ruma::events::typing::SyncTypingEvent; +use matrix_sdk::sync::SyncResponse; + +pub type BroadcastSender = tokio::sync::broadcast::Sender; + +#[derive(Debug, Clone)] +pub struct BxRoomEvent { + pub user: UserEmail, + pub data: Box, + pub room: Room, +} + +/// Broadcast messages +#[derive(Debug, Clone)] +pub enum BroadcastMessage { + /// User is or has been disconnected from Matrix + UserDisconnectedFromMatrix(UserEmail), + /// API token has been deleted + APITokenDeleted(APIToken), + /// Request a Matrix sync thread to be interrupted + StopSyncThread(MatrixSyncTaskID), + /// Matrix sync thread has been interrupted + SyncThreadStopped(MatrixSyncTaskID), + /// New room message + RoomMessageEvent(BxRoomEvent), + /// New reaction message + ReactionEvent(BxRoomEvent), + /// New room redaction + 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/constants.rs b/matrixgw_backend/src/constants.rs new file mode 100644 index 0000000..93ee5b7 --- /dev/null +++ b/matrixgw_backend/src/constants.rs @@ -0,0 +1,28 @@ +use std::time::Duration; + +/// 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; + +/// Length of generated tokens +pub const TOKENS_LEN: usize = 50; + +/// 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"; +} + +/// 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/auth_controller.rs b/matrixgw_backend/src/controllers/auth_controller.rs new file mode 100644 index 0000000..d9332b0 --- /dev/null +++ b/matrixgw_backend/src/controllers/auth_controller.rs @@ -0,0 +1,137 @@ +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; +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(client: MatrixClientExtractor) -> HttpResult { + Ok(HttpResponse::Ok().json(client.to_extended_user_info().await?)) +} + +/// Sign out user +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, &tx).await?; + } + + AuthenticatedMethod::Dev => { + // Nothing to be done, user is always authenticated + } + } + + Ok(HttpResponse::NoContent().finish()) +} 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..93baf63 --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/matrix_event_controller.rs @@ -0,0 +1,302 @@ +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::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::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; +use matrix_sdk::ruma::events::relation::Annotation; +use matrix_sdk::ruma::events::room::message::{ + 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; + +#[derive(Serialize)] +pub struct APIEvent { + pub 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 events: Vec, +} + +/// Get messages for a given room +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 { + start: messages.start, + end: messages.end, + events: stream::iter(messages.chunk) + .then(async |msg| APIEvent::from_evt(msg, room.room_id()).await) + .collect::>() + .await + .into_iter() + .collect::, _>>()?, + }) +} + +#[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.room_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(), + None, + ) + .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.room_id) else { + return Ok(HttpResponse::NotFound().json("Room not found!")); + }; + + room.send(RoomMessageEventContent::text_plain(req.content)) + .await?; + + Ok(HttpResponse::Accepted().finish()) +} + +#[derive(serde::Deserialize)] +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 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), + }; + + 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, +} + +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, + 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}")) + } + }) +} + +/// 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::Main, + event_path.event_id.clone(), + ) + .await?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs new file mode 100644 index 0000000..659b910 --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/matrix_media_controller.rs @@ -0,0 +1,88 @@ +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::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)] +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)] + 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, source: MediaSource, thumbnail: bool) -> HttpResult { + let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?; + + let media = client + .client + .client + .media() + .get_media_content( + &MediaRequestParameters { + source: source.clone(), + format: match thumbnail { + true => MediaFormat::Thumbnail(MediaThumbnailSettings::new( + UInt::new(100).unwrap(), + UInt::new(100).unwrap(), + )), + false => MediaFormat::File, + }, + }, + true, + ) + .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()); + + // 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/matrix_profile_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs new file mode 100644 index 0000000..a5cea9e --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/matrix_profile_controller.rs @@ -0,0 +1,67 @@ +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}; + +#[derive(serde::Deserialize)] +pub struct UserIDInPath { + user_id: OwnedUserId, +} + +#[derive(serde::Serialize)] +struct ProfileResponse { + user_id: OwnedUserId, + display_name: Option, + avatar: Option, +} + +impl ProfileResponse { + 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::()?, + }) + } +} + +/// 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(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/controllers/matrix/matrix_room_controller.rs b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs new file mode 100644 index 0000000..cd63c9b --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/matrix_room_controller.rs @@ -0,0 +1,180 @@ +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}; +use futures_util::{StreamExt, stream}; +use matrix_sdk::notification_settings::{ + IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode, +}; +use matrix_sdk::room::ParentSpace; +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)] +pub struct APIRoomInfo { + id: OwnedRoomId, + name: Option, + members: Vec, + avatar: Option, + is_space: bool, + parents: Vec, + number_unread_messages: u64, + notifications: RoomNotificationMode, + latest_event: Option, +} + +impl APIRoomInfo { + async fn from_room(r: &Room, notif: &NotificationSettings) -> anyhow::Result { + // Get parent spaces + 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::>(); + + 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, + 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, None) + .await? + .events + .into_iter() + .next(), + }) + } +} + +/// 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, ¬ifs).await) + .collect::>() + .await + .into_iter() + .collect::, _>>()?; + + Ok(HttpResponse::Ok().json(list)) +} + +/// 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, ¬ifs).await) + .collect::>() + .await + .into_iter() + .collect::, _>>()?; + + Ok(HttpResponse::Ok().json(list)) +} + +#[derive(serde::Deserialize)] +pub struct RoomIdInPath { + pub(crate) room_id: OwnedRoomId, +} + +/// Get the list of joined rooms of the user +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, ¬ifs).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.room_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")); + }; + + 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/controllers/matrix/mod.rs b/matrixgw_backend/src/controllers/matrix/mod.rs new file mode 100644 index 0000000..3a34b16 --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix/mod.rs @@ -0,0 +1,4 @@ +pub mod matrix_event_controller; +pub mod matrix_media_controller; +pub mod matrix_profile_controller; +pub mod matrix_room_controller; 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..d4e507e --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix_link_controller.rs @@ -0,0 +1,59 @@ +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 crate::matrix_connection::matrix_manager::MatrixManagerMsg; +use actix_web::{HttpResponse, web}; +use ractor::ActorRef; + +#[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 })) +} + +/// Finish user authentication on Matrix server +pub async fn finish_auth(client: MatrixClientExtractor) -> HttpResult { + match client + .client + .finish_login(client.auth.decode_json_body::()?) + .await + { + Ok(_) => Ok(HttpResponse::Accepted().finish()), + Err(e) => { + log::error!("Failed to finish Matrix authentication: {e}"); + Err(e.into()) + } + } +} + +/// 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()) +} + +#[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/controllers/matrix_sync_thread_controller.rs b/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs new file mode 100644 index 0000000..a071701 --- /dev/null +++ b/matrixgw_backend/src/controllers/matrix_sync_thread_controller.rs @@ -0,0 +1,59 @@ +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()) + } + } +} + +/// 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()) + } + } +} + +#[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/src/server/mod.rs b/matrixgw_backend/src/controllers/mod.rs similarity index 53% rename from src/server/mod.rs rename to matrixgw_backend/src/controllers/mod.rs index 007cd9f..9dfcf05 100644 --- a/src/server/mod.rs +++ b/matrixgw_backend/src/controllers/mod.rs @@ -1,10 +1,15 @@ 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; +pub mod auth_controller; +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; #[derive(thiserror::Error, Debug)] pub enum HttpFailure { @@ -12,24 +17,20 @@ pub enum HttpFailure { 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), + #[error("Actix web error: {0}")] + 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 { @@ -42,7 +43,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/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/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/controllers/tokens_controller.rs b/matrixgw_backend/src/controllers/tokens_controller.rs new file mode 100644 index 0000000..ce50d82 --- /dev/null +++ b/matrixgw_backend/src/controllers/tokens_controller.rs @@ -0,0 +1,53 @@ +use crate::broadcast_messages::BroadcastSender; +use crate::controllers::HttpResult; +use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod}; +use crate::users::{APIToken, APITokenID, BaseAPIToken}; +use actix_web::{HttpResponse, web}; + +/// 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::>(), + )) +} + +#[derive(serde::Deserialize)] +pub struct TokenIDInPath { + id: APITokenID, +} + +/// Delete an API access token +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, &tx).await?; + Ok(HttpResponse::Accepted().finish()) +} diff --git a/matrixgw_backend/src/controllers/ws_controller.rs b/matrixgw_backend/src/controllers/ws_controller.rs new file mode 100644 index 0000000..57a80de --- /dev/null +++ b/matrixgw_backend/src/controllers/ws_controller.rs @@ -0,0 +1,307 @@ +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 crate::users::UserEmail; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest, HttpResponse, web}; +use actix_ws::Message; +use futures_util::StreamExt; +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; +use tokio::sync::broadcast::Receiver; +use tokio::time::interval; + +#[derive(Debug, serde::Serialize)] +pub struct WsRoomEvent { + pub room_id: OwnedRoomId, + pub event_id: OwnedEventId, + pub sender: OwnedUserId, + pub origin_server_ts: MilliSecondsSinceUnixEpoch, + 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, +} + +#[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")] +pub enum WsMessage { + /// Room message event + RoomMessageEvent(WsRoomEvent), + + /// Room reaction event + RoomReactionEvent(WsRoomEvent), + + /// Room reaction event + RoomRedactionEvent(WsRoomEvent), + + /// Fully read message event + ReceiptEvent(WsReceiptEvent), + + /// User is typing event + TypingEvent(WsTypingEvent), +} + +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 { + 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 { + 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 { + 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::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, + })) + } + + 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, + } + } +} + +/// 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?; + + // 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, + 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, WsMessage::from_bx_message(&msg, &auth.user.email)) { + (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; + } + + (_, Some(message)) => { + // Send the message to the websocket + if let Ok(msg) = serde_json::to_string(&message) + && 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 new file mode 100644 index 0000000..339ca5d --- /dev/null +++ b/matrixgw_backend/src/extractors/auth_extractor.rs @@ -0,0 +1,334 @@ +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 jwt_simple::reexports::serde_json; +use serde::de::DeserializeOwned; +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), +} + +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, + 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 + .payload + .as_ref() + .context("Failed to decode request as json: missing payload!")?; + Ok(serde_json::from_slice(payload)?) + } +} + +#[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(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:?}", + token.id + ); + return Err(actix_web::error::ErrorForbidden( + "This token cannot be used from this IP address!", + )); + } + + // Check for write access + if token.base.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/matrix_client_extractor.rs b/matrixgw_backend/src/extractors/matrix_client_extractor.rs new file mode 100644 index 0000000..8ba5688 --- /dev/null +++ b/matrixgw_backend/src/extractors/matrix_client_extractor.rs @@ -0,0 +1,54 @@ +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; + +pub struct MatrixClientExtractor { + pub auth: AuthExtractor, + pub client: MatrixClient, +} + +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(), + }) + } +} + +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() + ); + + 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/extractors/mod.rs b/matrixgw_backend/src/extractors/mod.rs new file mode 100644 index 0000000..546e4d2 --- /dev/null +++ b/matrixgw_backend/src/extractors/mod.rs @@ -0,0 +1,3 @@ +pub mod auth_extractor; +pub mod matrix_client_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/src/lib.rs b/matrixgw_backend/src/lib.rs similarity index 61% rename from src/lib.rs rename to matrixgw_backend/src/lib.rs index e0979b6..3eb69b6 100644 --- a/src/lib.rs +++ b/matrixgw_backend/src/lib.rs @@ -1,8 +1,8 @@ pub mod app_config; pub mod broadcast_messages; pub mod constants; +pub mod controllers; pub mod extractors; -pub mod server; -pub mod sync_client; -pub mod user; +pub mod matrix_connection; +pub mod users; pub mod utils; diff --git a/matrixgw_backend/src/main.rs b/matrixgw_backend/src/main.rs new file mode 100644 index 0000000..d2bcccf --- /dev/null +++ b/matrixgw_backend/src/main.rs @@ -0,0 +1,222 @@ +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::broadcast_messages::BroadcastMessage; +use matrixgw_backend::constants; +use matrixgw_backend::controllers::matrix::{ + 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, + static_controller, tokens_controller, ws_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<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + 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!"); + + 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") + .await + .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, + ws_tx.clone(), + ) + .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()) + .session_lifecycle(SessionLifecycle::BrowserSession(Default::default())) + .build(); + + let cors = Cors::default() + .allowed_origin(&AppConfig::get().website_origin) + .allowed_methods(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) + .allowed_header(constants::API_AUTH_HEADER) + .allow_any_header() + .supports_credentials() + .max_age(3600); + + App::new() + .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(), + })) + .app_data(web::Data::new(ws_tx.clone())) + // Server controller + .route("/robots.txt", web::get().to(server_controller::robots_txt)) + .route( + "/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), + ) + // Matrix link controller + .route( + "/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), + ) + .route( + "/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), + ) + // 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), + ) + // Matrix synchronization controller + .route( + "/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), + ) + .route( + "/api/matrix_sync/status", + 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/joined_spaces", + web::get().to(matrix_room_controller::get_joined_spaces), + ) + .route( + "/api/matrix/room/{room_id}", + web::get().to(matrix_room_controller::single_room_info), + ) + .route( + "/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}", + web::get().to(matrix_profile_controller::get_profile), + ) + .route( + "/api/matrix/profile/get_multiple", + web::post().to(matrix_profile_controller::get_multiple), + ) + // Matrix events controller + .route( + "/api/matrix/room/{room_id}/events", + web::get().to(matrix_event_controller::get_for_room), + ) + .route( + "/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}/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), + ) + .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( + "/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)? + .run() + .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..4ec021a --- /dev/null +++ b/matrixgw_backend/src/matrix_connection/matrix_client.rs @@ -0,0 +1,401 @@ +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 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::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, SendOutsideWasm}; +use ractor::ActorRef; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; +use std::pin::Pin; +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, +} + +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +pub enum EncryptionRecoveryState { + Unknown, + Enabled, + Disabled, + Incomplete, +} + +/// 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}")] + 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 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}")] + 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}")] + BuildAuthRequest(OAuthError), + #[error("Failed to finalize authentication! {0}")] + 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), + #[error("Failed to set recovery key! {0}")] + SetRecoveryKey(matrix_sdk::encryption::recovery::RecoveryError), +} + +#[derive(serde::Deserialize)] +pub struct FinishMatrixAuth { + code: String, + state: String, +} + +#[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( + 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(); + 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)?; + + // 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() + .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)?; + + let client = Self { + manager, + email: email.clone(), + client, + }; + + // Check metadata + 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)? + .as_str(), + ) + .map_err(MatrixClientError::DecodeStoredSession)?; + + // Restore session + client + .client + .restore_session(OAuthSession { + client_id: session.client_id, + user: session.user_session, + }) + .await + .map_err(MatrixClientError::RestoreSession)?; + + // Wait for encryption tasks to complete + client + .client + .encryption() + .wait_for_e2ee_initialization_tasks() + .await; + + // Save stored session once + client.save_stored_session().await?; + } + + // Automatically save session when token gets refreshed + client.setup_background_session_save().await; + + Ok(client) + } + + /// 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(email); + if passphrase_path.is_file() { + std::fs::remove_file(passphrase_path).map_err(MatrixClientError::ClearDbPassphrase)?; + } + + Ok(()) + } + + /// 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) + } + + /// 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() + ); + + // 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(()) + } + + /// Automatically persist session onto disk + pub async fn setup_background_session_save(&self) { + let this = self.clone(); + tokio::spawn(async move { + 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; + } + 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; + } + } + } + }); + } + + /// 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 full_session = self + .client + .oauth() + .full_session() + .context("A logged in client must have a session")?; + + let stored_session = StoredSession { + user_session: full_session.user, + client_id: full_session.client_id, + }; + + 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(()) + } + + /// 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 { + log::warn!("Failed to send logout request: {e}"); + } + + // Destroy user associated data + Self::destroy_data(&self.email)?; + + 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() { + RecoveryState::Unknown => EncryptionRecoveryState::Unknown, + RecoveryState::Enabled => EncryptionRecoveryState::Enabled, + RecoveryState::Disabled => EncryptionRecoveryState::Disabled, + 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)?) + } + + /// 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) + } + + /// 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 new file mode 100644 index 0000000..77fc707 --- /dev/null +++ b/matrixgw_backend/src/matrix_connection/matrix_manager.rs @@ -0,0 +1,164 @@ +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; + +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), + StopSyncThread(UserEmail), + SyncThreadGetStatus(UserEmail, RpcReplyPort), + SyncThreadTerminated(UserEmail, MatrixSyncTaskID), +} + +pub struct MatrixManagerActor; + +impl Actor for MatrixManagerActor { + type Msg = MatrixManagerMsg; + type State = MatrixManagerState; + type Arguments = BroadcastSender; + + async fn pre_start( + &self, + _myself: ActorRef, + args: Self::Arguments, + ) -> Result { + Ok(MatrixManagerState { + broadcast_sender: args, + clients: HashMap::new(), + running_sync_threads: Default::default(), + }) + } + + 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, + 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(myself, &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}") + } + } + 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}"); + } + if let Err(e) = state + .broadcast_sender + .send(BroadcastMessage::UserDisconnectedFromMatrix(email)) + { + log::warn!( + "Failed to notify that user has been disconnected from Matrix! {e}" + ); + } + } + } + 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(()); + }; + + 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 = + 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) => { + 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::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!( + "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/mod.rs b/matrixgw_backend/src/matrix_connection/mod.rs new file mode 100644 index 0000000..29194b1 --- /dev/null +++ b/matrixgw_backend/src/matrix_connection/mod.rs @@ -0,0 +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..73df0c4 --- /dev/null +++ b/matrixgw_backend/src/matrix_connection/sync_thread.rs @@ -0,0 +1,185 @@ +//! # Matrix sync thread +//! +//! This file contains the logic performed by the threads that synchronize with Matrix account. + +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; +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::ruma::events::typing::SyncTypingEvent; +use ractor::ActorRef; + +#[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, + 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(); + + tokio::task::spawn(async move { + 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, + manager: ActorRef, +) { + let mut rx = tx.subscribe(); + log::info!("Sync thread {id:?} started for user {:?}", client.email); + + let mut sync_stream = client.sync_stream().await; + + let mut handlers = vec![]; + + let tx_msg_handle = tx.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_mail.clone(), + data: Box::new(event), + room, + })) { + log::warn!("Failed to forward room message event! {e}"); + } + }, + )); + + let tx_reac_handle = tx.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_mail.clone(), + data: Box::new(event), + room, + })) { + log::warn!("Failed to forward reaction event! {e}"); + } + }, + )); + + let tx_redac_handle = tx.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_mail.clone(), + data: Box::new(event), + room, + })) + { + log::warn!("Failed to forward redaction event! {e}"); + } + }, + )); + + let tx_receipt_handle = tx.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_mail.clone(), + data: Box::new(event), + room, + })) { + log::warn!("Failed to forward receipt event! {e}"); + } + }), + ); + + 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 + 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}"); + break; + } + Ok(_) => {} + } + } + + res = sync_stream.next() => { + let Some(res)= res else { + log::error!("No more Matrix event to process, stopping now..."); + break; + }; + + // 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); + } + } + } + } + } + + 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!"); + 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}") + } +} diff --git a/matrixgw_backend/src/users.rs b/matrixgw_backend/src/users.rs new file mode 100644 index 0000000..6727e0b --- /dev/null +++ b/matrixgw_backend/src/users.rs @@ -0,0 +1,288 @@ +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; +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; + +/// 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 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}")] + 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) + } +} + +/// Base API token information +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] +pub struct BaseAPIToken { + /// Token name + pub name: String, + + /// Restricted API network for token + pub networks: Option>, + + /// Token max inactivity + pub max_inactivity: u32, + + /// Token expiration + pub expiration: Option, + + /// Read only access + pub read_only: bool, +} + +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, + + /// Client creation time + pub created: u64, + + /// Client last usage time + pub last_used: 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); + 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, 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(()) + } + + pub fn shall_update_time_used(&self) -> bool { + let refresh_interval = min(600, self.base.max_inactivity / 10); + + (self.last_used) < time_secs() - refresh_interval as u64 + } + + pub fn is_expired(&self) -> bool { + // 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() + } +} + +#[derive(serde::Serialize, Debug, Clone)] +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_backend/src/utils/crypt_utils.rs b/matrixgw_backend/src/utils/crypt_utils.rs new file mode 100644 index 0000000..f1cff8c --- /dev/null +++ b/matrixgw_backend/src/utils/crypt_utils.rs @@ -0,0 +1,11 @@ +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)) +} 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() +} 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/.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..f5e6f33 --- /dev/null +++ b/matrixgw_frontend/README.md @@ -0,0 +1,2 @@ +# MatrixGW frontend +Built using React + TypeScript + Vite \ No newline at end of file diff --git a/matrixgw_frontend/eslint.config.js b/matrixgw_frontend/eslint.config.js new file mode 100644 index 0000000..7da0abe --- /dev/null +++ b/matrixgw_frontend/eslint.config.js @@ -0,0 +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"; + +export default defineConfig([ + globalIgnores(["dist"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + rules: { + "react-refresh/only-export-components": "off", + }, + }, +]); 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..5d7e649 --- /dev/null +++ b/matrixgw_frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + MatrixGW + + +
+ + + diff --git a/matrixgw_frontend/package-lock.json b/matrixgw_frontend/package-lock.json new file mode 100644 index 0000000..77ae474 --- /dev/null +++ b/matrixgw_frontend/package-lock.json @@ -0,0 +1,4037 @@ +{ + "name": "matrixgw_frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "matrixgw_frontend", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource/roboto": "^5.2.9", + "@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", + "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.10.0" + }, + "devDependencies": { + "@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.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.1", + "vite": "npm:rolldown-vite@7.2.10" + } + }, + "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==", + "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", + "peer": true, + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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/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==", + "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==", + "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==", + "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.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "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, + "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/@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", + "peer": true, + "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", + "peer": true, + "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", + "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/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/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.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": { + "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/@fontsource/roboto": { + "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" + } + }, + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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", + "integrity": "sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "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" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "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.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", + "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.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" + }, + "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.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.6", + "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.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", + "@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.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.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" + }, + "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.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" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "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.9", + "@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/@mui/x-data-grid": { + "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.19.0", + "@mui/x-virtualizer": "0.2.10", + "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-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", + "@mui/x-internals": "8.19.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.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.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-virtualizer": { + "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.19.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/@napi-rs/wasm-runtime": { + "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.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@oxc-project/runtime": { + "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": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "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": { + "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.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" + ], + "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.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" + ], + "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.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" + ], + "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.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" + ], + "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.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" + ], + "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.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" + ], + "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.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" + ], + "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.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" + ], + "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.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" + ], + "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.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" + ], + "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.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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "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" + ], + "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.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" + ], + "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.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "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.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, + "dependencies": { + "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.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.2.2" + } + }, + "node_modules/@types/react-dom": { + "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": { + "@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.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.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", + "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.48.1", + "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.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.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": { + "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.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.48.1", + "@typescript-eslint/types": "^8.48.1", + "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.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.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" + }, + "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.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": { + "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.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.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" + }, + "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.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": { + "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.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.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", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "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.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.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" + }, + "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.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.48.1", + "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.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.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.47", + "@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", + "peer": true, + "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/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/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", + "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/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", + "peer": true, + "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==", + "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/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", + "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", + "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/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", + "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", + "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.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.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" + } + }, + "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", + "peer": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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/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", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "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", + "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", + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "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, + "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.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@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", + "@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": "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": ">=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" + } + }, + "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-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/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/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/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", + "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/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", + "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/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", + "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.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "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/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/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", + "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", + "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==", + "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/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", + "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", + "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/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==", + "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==", + "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-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", + "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/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", + "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/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", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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==", + "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/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", + "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==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "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", + "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/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==", + "license": "ISC" + }, + "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/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/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", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "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/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "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", + "peer": true, + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "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", + "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", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "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", + "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", + "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/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", + "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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "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.101.0", + "@rolldown/pluginutils": "1.0.0-beta.53" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@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.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/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/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", + "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": { + "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", + "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/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", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "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", + "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/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", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "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.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" + }, + "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/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.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.101.0", + "fdir": "^6.5.0", + "lightningcss": "^1.30.2", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rolldown": "1.0.0-beta.53", + "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/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" + } + }, + "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 new file mode 100644 index 0000000..cf6de10 --- /dev/null +++ b/matrixgw_frontend/package.json @@ -0,0 +1,48 @@ +{ + "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": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@fontsource/roboto": "^5.2.9", + "@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", + "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.10.0" + }, + "devDependencies": { + "@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.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.48.1", + "vite": "npm:rolldown-vite@7.2.10" + }, + "overrides": { + "vite": "npm:rolldown-vite@7.2.10" + } +} diff --git a/matrixgw_frontend/public/favicon.png b/matrixgw_frontend/public/favicon.png new file mode 100644 index 0000000..6027e50 Binary files /dev/null and b/matrixgw_frontend/public/favicon.png differ diff --git a/matrixgw_frontend/src/App.tsx b/matrixgw_frontend/src/App.tsx new file mode 100644 index 0000000..71e9385 --- /dev/null +++ b/matrixgw_frontend/src/App.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { + createBrowserRouter, + createRoutesFromElements, + Route, + RouterProvider, +} 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 { WSDebugRoute } from "./routes/WSDebugRoute"; +import { BaseLoginPage } from "./widgets/auth/BaseLoginPage"; +import BaseAuthenticatedPage from "./widgets/dashboard/BaseAuthenticatedPage"; + +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/api/ApiClient.ts b/matrixgw_frontend/src/api/ApiClient.ts new file mode 100644 index 0000000..fcd4f11 --- /dev/null +++ b/matrixgw_frontend/src/api/ApiClient.ts @@ -0,0 +1,193 @@ +import { AuthApi } from "./AuthApi"; + +interface RequestParams { + uri: string; + method: "GET" | "POST" | "DELETE" | "PATCH" | "PUT"; + allowFail?: boolean; + jsonData?: unknown; + formData?: FormData; + upProgress?: (progress: number) => void; + downProgress?: (e: { progress: number; total: number }) => void; +} + +interface APIResponse { + data: unknown; + status: number; +} + +export class ApiError extends Error { + public code: number; + public data: unknown; + constructor(message: string, code: number, data: unknown) { + 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; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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..5d5c926 --- /dev/null +++ b/matrixgw_frontend/src/api/AuthApi.ts @@ -0,0 +1,91 @@ +import { APIClient } from "./ApiClient"; + +export interface UserInfo { + id: number; + time_create: number; + 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"; +} + +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 as { url: string }; + } + + /** + * 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 user information + */ + static async GetUserInfo(): Promise { + return ( + await APIClient.exec({ + uri: "/auth/info", + method: "GET", + }) + ).data as UserInfo; + } + + /** + * 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/MatrixLinkApi.ts b/matrixgw_frontend/src/api/MatrixLinkApi.ts new file mode 100644 index 0000000..7fc1f83 --- /dev/null +++ b/matrixgw_frontend/src/api/MatrixLinkApi.ts @@ -0,0 +1,47 @@ +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 as { url: string }; + } + + /** + * 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 }, + }); + } + + /** + * Disconnect from Matrix Account + */ + static async Disconnect(): Promise { + await APIClient.exec({ + uri: "/matrix_link/logout", + 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/api/MatrixSyncApi.ts b/matrixgw_frontend/src/api/MatrixSyncApi.ts new file mode 100644 index 0000000..509ff85 --- /dev/null +++ b/matrixgw_frontend/src/api/MatrixSyncApi.ts @@ -0,0 +1,34 @@ +import { APIClient } from "./ApiClient"; + +export class MatrixSyncApi { + /** + * Start sync thread + */ + static async Start(): Promise { + await APIClient.exec({ + method: "POST", + 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 as { started: boolean }).started; + } +} diff --git a/matrixgw_frontend/src/api/ServerApi.ts b/matrixgw_frontend/src/api/ServerApi.ts new file mode 100644 index 0000000..50f52a1 --- /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 as ServerConfig; + } + + /** + * Get cached configuration + */ + static get Config(): ServerConfig { + if (config === null) throw new Error("Missing configuration!"); + return config; + } +} diff --git a/matrixgw_frontend/src/api/TokensApi.ts b/matrixgw_frontend/src/api/TokensApi.ts new file mode 100644 index 0000000..92f23c2 --- /dev/null +++ b/matrixgw_frontend/src/api/TokensApi.ts @@ -0,0 +1,56 @@ +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 as Token[]; + } + + /** + * Create a new token + */ + static async Create(t: BaseToken): Promise { + return ( + await APIClient.exec({ + uri: "/token", + method: "POST", + jsonData: t, + }) + ).data as TokenWithSecret; + } + + /** + * Delete a token + */ + static async Delete(t: Token): Promise { + await APIClient.exec({ + uri: `/token/${t.id}`, + method: "DELETE", + }); + } +} diff --git a/matrixgw_frontend/src/api/WsApi.ts b/matrixgw_frontend/src/api/WsApi.ts new file mode 100644 index 0000000..334ca21 --- /dev/null +++ b/matrixgw_frontend/src/api/WsApi.ts @@ -0,0 +1,82 @@ +import { APIClient } from "./ApiClient"; +import type { MessageType } from "./matrix/MatrixApiEvent"; + +interface BaseRoomEvent { + time: number; + room_id: string; + event_id: string; + sender: string; + origin_server_ts: number; +} + +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; + }; + url?: 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 interface ReceiptEventEntry { + event: string; + user: string; + ts?: number; +} + +export interface RoomReceiptEvent { + time: number; + type: "ReceiptEvent"; + room_id: string; + receipts: ReceiptEventEntry[]; +} + +export interface RoomTypingEvent { + time: number; + type: "TypingEvent"; + room_id: string; + user_ids: string[]; +} + +export type WsMessage = + | RoomMessageEvent + | RoomReactionEvent + | RoomRedactionEvent + | RoomReceiptEvent + | RoomTypingEvent; + +export class WsApi { + /** + * Get WebSocket URL + */ + static get WsURL(): string { + return APIClient.backendURL() + "/ws"; + } +} diff --git a/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts new file mode 100644 index 0000000..34d31d7 --- /dev/null +++ b/matrixgw_frontend/src/api/matrix/MatrixApiEvent.ts @@ -0,0 +1,153 @@ +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: MessageType; + "m.relates_to"?: { + event_id: string; + rel_type: "m.replace" | string; + }; + url?: 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 as MatrixEventsList; + } + + /** + * 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}`; + } + + /** + * 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 }, + }); + } + + /** + * 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 }, + }); + } + + /** + * 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 + */ + static async DeleteEvent(room: Room, event_id: string): Promise { + await APIClient.exec({ + method: "DELETE", + 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/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..3301c60 --- /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 = ( + await APIClient.exec({ + method: "POST", + uri: "/matrix/profile/get_multiple", + jsonData: ids, + }) + ).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 new file mode 100644 index 0000000..3d5d116 --- /dev/null +++ b/matrixgw_frontend/src/api/matrix/MatrixApiRoom.ts @@ -0,0 +1,74 @@ +import { APIClient } from "../ApiClient"; +import type { UserInfo } from "../AuthApi"; +import type { MatrixEvent } from "./MatrixApiEvent"; +import type { UsersMap } from "./MatrixApiProfile"; + +export interface Room { + id: string; + name?: string; + members: string[]; + avatar?: string; + is_space?: boolean; + parents: string[]; + number_unread_messages: number; + notifications: "AllMessages" | "MentionsAndKeywordsOnly" | "Mute"; + latest_event?: MatrixEvent; +} + +export interface Receipt { + user: string; + event_id: string; + ts: number; +} + +/** + * 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 + */ + static async ListJoined(): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: "/matrix/room/joined", + }) + ).data as Room[]; + } + + /** + * Get a room receipts + */ + static async RoomReceipts(room: Room): Promise { + return ( + await APIClient.exec({ + method: "GET", + uri: `/matrix/room/${room.id}/receipts`, + }) + ).data as Receipt[]; + } +} 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/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/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..6338de7 --- /dev/null +++ b/matrixgw_frontend/src/hooks/contexts_provider/LoadingMessageProvider.tsx @@ -0,0 +1,61 @@ +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(0); + + const [message, setMessage] = React.useState(""); + + const hook: LoadingMessageContext = { + show(message) { + setMessage(message); + setOpen((v) => v + 1); + }, + hide() { + setOpen((v) => v - 1); + }, + }; + + return ( + <> + {p.children} + + 0}> + + +
+ + + {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/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/index.css b/matrixgw_frontend/src/index.css new file mode 100644 index 0000000..ba4562c --- /dev/null +++ b/matrixgw_frontend/src/index.css @@ -0,0 +1,18 @@ +body { + margin: 0; +} + +html, +body, +#root { + height: 100%; +} + +#root { + display: flex; + flex-direction: column; +} + +#root > div { + flex: 1; +} diff --git a/matrixgw_frontend/src/main.tsx b/matrixgw_frontend/src/main.tsx new file mode 100644 index 0000000..6508bc0 --- /dev/null +++ b/matrixgw_frontend/src/main.tsx @@ -0,0 +1,45 @@ +import "@fontsource/roboto/300.css"; +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 { ServerApi } from "./api/ServerApi"; +import { App } from "./App"; +import { AlertDialogProvider } from "./hooks/contexts_provider/AlertDialogProvider"; +import { ConfirmDialogProvider } from "./hooks/contexts_provider/ConfirmDialogProvider"; +import { LoadingMessageProvider } from "./hooks/contexts_provider/LoadingMessageProvider"; +import { SnackbarProvider } from "./hooks/contexts_provider/SnackbarProvider"; +import "./index.css"; +import { AppTheme } from "./theme/AppTheme"; +import { AsyncWidget } from "./widgets/AsyncWidget"; + +createRoot(document.getElementById("root")!).render( + + + + + + + + + { + 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..d9166e2 --- /dev/null +++ b/matrixgw_frontend/src/routes/APITokensRoute.tsx @@ -0,0 +1,293 @@ +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, GridActionsCellItem } from "@mui/x-data-grid"; +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"; +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, setCount] = React.useState(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 = () => { + setCount((c) => c + 1); + setList(undefined); + }; + + const handleOpenCreateTokenDialog = () => setOpenCreateTokenDialog(true); + + const handleCancelCreateToken = () => setOpenCreateTokenDialog(false); + + const handleCreatedToken = (s: TokenWithSecret) => { + setCreatedToken(s); + setOpenCreateTokenDialog(false); + handleRefreshTokensList(); + }; + + return ( + + + + + + +    + + + + + + + } + > + {/* Create token dialog anchor */} + + + {/* Info about created token */} + {createdToken && } + + {/* Tokens 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: +
+
+
+ ); +} + +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 }, + { + 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", + }, + { + 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) + return ( +
+ You do not have created any token yet! +
+ ); + + return ( + c.id} + isCellEditable={() => false} + isRowSelectable={() => false} + /> + ); +} diff --git a/matrixgw_frontend/src/routes/HomeRoute.tsx b/matrixgw_frontend/src/routes/HomeRoute.tsx new file mode 100644 index 0000000..f046f41 --- /dev/null +++ b/matrixgw_frontend/src/routes/HomeRoute.tsx @@ -0,0 +1,11 @@ +import { useUserInfo } from "../widgets/dashboard/BaseAuthenticatedPage"; +import { MainMessageWidget } from "../widgets/messages/MainMessagesWidget"; +import { NotLinkedAccountMessage } from "../widgets/NotLinkedAccountMessage"; + +export function HomeRoute(): React.ReactElement { + const user = useUserInfo(); + + if (!user.info.matrix_account_connected) return ; + + return ; +} diff --git a/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx new file mode 100644 index 0000000..1d5aa19 --- /dev/null +++ b/matrixgw_frontend/src/routes/MatrixAuthCallback.tsx @@ -0,0 +1,81 @@ +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!); + + snackbar("Successfully linked to Matrix account!"); + navigate("/matrix_link"); + } catch (e) { + console.error(e); + setError(String(e)); + } finally { + info.reloadUserInfo(); + } + }; + + load(); + }, [code, info, navigate, snackbar, state]); + + 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 new file mode 100644 index 0000000..0125fad --- /dev/null +++ b/matrixgw_frontend/src/routes/MatrixLinkRoute.tsx @@ -0,0 +1,329 @@ +import CheckIcon from "@mui/icons-material/Check"; +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"; +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"; + +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 snackbar = useSnackbar(); + const confirm = useConfirm(); + const alert = useAlert(); + const loadingMessage = useLoadingMessage(); + + 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 { + user.reloadUserInfo(); + loadingMessage.hide(); + } + }; + + return ( + + + + Connected to your Matrix account + + + +

+ 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. +

+
+
+ + + +
+ ); +} + +function EncryptionKeyStatus(): React.ReactElement { + const user = useUserInfo(); + + const [openSetKeyDialog, setOpenSetKeyDialog] = React.useState(false); + + const handleSetKey = () => setOpenSetKeyDialog(true); + const handleCloseSetKey = () => setOpenSetKeyDialog(false); + + 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 */} + + + ); +} + +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); + + return () => 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 && ( + + )} + +
+ + ); +} 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/WSDebugRoute.tsx b/matrixgw_frontend/src/routes/WSDebugRoute.tsx new file mode 100644 index 0000000..56cd9e9 --- /dev/null +++ b/matrixgw_frontend/src/routes/WSDebugRoute.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import { JsonView, darkStyles } from "react-json-view-lite"; +import "react-json-view-lite/dist/index.css"; +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"; + +type TimestampedMessages = WsMessage & { time: number }; + +export function WSDebugRoute(): React.ReactElement { + const user = useUserInfo(); + + const [state, setState] = React.useState(WSState.Closed); + const [messages, setMessages] = React.useState([]); + + const handleMessage = (msg: WsMessage) => { + setMessages((l) => [...l, msg]); + }; + + if (!user.info.matrix_account_connected) return ; + + return ( + + {/* Status bar */} +
+ State: + + {state} + + +
+ + {/* WS messages list */} + {messages.map((msg, id) => ( +
+ level < 2} + style={{ + ...darkStyles, + container: "", + }} + /> +
+ ))} +
+ ); +} diff --git a/matrixgw_frontend/src/routes/auth/LoginRoute.tsx b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx new file mode 100644 index 0000000..e6f476e --- /dev/null +++ b/matrixgw_frontend/src/routes/auth/LoginRoute.tsx @@ -0,0 +1,50 @@ +import { Alert, Box, Button, CircularProgress } from "@mui/material"; +import React from "react"; +import { AuthApi } from "../../api/AuthApi"; +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); + 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 new file mode 100644 index 0000000..82f80fe --- /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/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..a3ba6b6 --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/dataDisplay.tsx @@ -0,0 +1,232 @@ +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"; + +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..29a7e94 --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/feedback.tsx @@ -0,0 +1,45 @@ +import { type Theme, alpha, type Components } from "@mui/material/styles"; +import { gray, orange } from "../themePrimitives"; + +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..21c2b09 --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/inputs.tsx @@ -0,0 +1,451 @@ +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"; + +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..6b5cb2d --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/navigation.tsx @@ -0,0 +1,283 @@ +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"; + +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..9dbe938 --- /dev/null +++ b/matrixgw_frontend/src/theme/customizations/surfaces.ts @@ -0,0 +1,112 @@ +import { alpha, type Theme, type Components } from "@mui/material/styles"; +import { gray } from "../themePrimitives"; + +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..b924844 --- /dev/null +++ b/matrixgw_frontend/src/theme/themePrimitives.ts @@ -0,0 +1,412 @@ +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 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, +}; + +const defaultShadows: Shadows = [ + "none", + "var(--template-palette-baseShadow)", + ...defaultTheme.shadows.slice(2), +] as never; + +export const shadows = defaultShadows; diff --git a/matrixgw_frontend/src/utils/DateUtils.ts b/matrixgw_frontend/src/utils/DateUtils.ts new file mode 100644 index 0000000..0bf9610 --- /dev/null +++ b/matrixgw_frontend/src/utils/DateUtils.ts @@ -0,0 +1,78 @@ +import { format } from "date-and-time"; + +/** + * Get UNIX time + * + * @returns Number of seconds since Epoch + */ +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(); +} + +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/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/utils/RoomEventsManager.ts b/matrixgw_frontend/src/utils/RoomEventsManager.ts new file mode 100644 index 0000000..2efb88a --- /dev/null +++ b/matrixgw_frontend/src/utils/RoomEventsManager.ts @@ -0,0 +1,245 @@ +import dayjs from "dayjs"; +import type { + MatrixEvent, + MatrixEventData, + MatrixEventsList, + MessageType, +} from "../api/matrix/MatrixApiEvent"; +import type { Receipt, Room } from "../api/matrix/MatrixApiRoom"; +import type { WsMessage } from "../api/WsApi"; +import { timeMs } from "./DateUtils"; + +export interface MessageReaction { + event_id: string; + account: string; + key: string; +} + +export interface Message { + event_id: string; + account: string; + time_sent: number; + time_sent_dayjs: dayjs.Dayjs; + modified: boolean; + reactions: Map; + content: string; + type: MessageType; + file?: string; +} + +export class RoomEventsManager { + readonly room: Room; + private events: MatrixEvent[]; + private receipts: Receipt[]; + messages: Message[]; + endToken?: string; + typingUsers: string[]; + receiptsEventsMap: Map; + + get canLoadOlder(): boolean { + return !!this.endToken; + } + + 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); + } + + /** + * Process events given by the API + */ + processNewEvents(evts: MatrixEventsList) { + this.endToken = evts.end; + this.events = [...this.events, ...evts.events]; + this.rebuildMessagesList(); + } + + processWsMessage(m: WsMessage) { + if (m.room_id !== this.room.id) { + console.debug("Not an event for current room."); + 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, + url: m.data.url, + 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; // Not a real event + } 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); + + // 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 + const 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; + } + + // Else it is a new message; update receipts if needed + else { + const 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, + modified: false, + reactions: new Map(), + time_sent: evt.time, + time_sent_dayjs: dayjs.unix(evt.time / 1000), + type: data.content.msgtype, + file: data.content.file?.url ?? data.content.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 + ); + const key = data.content["m.relates_to"].key; + + if (!message) continue; + + if (!message.reactions.has(key)) message.reactions.set(key, []); + + message.reactions.get(key)!.push({ + account: evt.sender, + event_id: evt.id, + key, + }); + } + } + + // Adapt receipts to be event-indexed + this.receiptsEventsMap.clear(); + 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/AsyncWidget.tsx b/matrixgw_frontend/src/widgets/AsyncWidget.tsx new file mode 100644 index 0000000..0085f52 --- /dev/null +++ b/matrixgw_frontend/src/widgets/AsyncWidget.tsx @@ -0,0 +1,82 @@ +import { Alert, Box, Button, CircularProgress } from "@mui/material"; +import React from "react"; + +const State = { + Loading: 0, + Ready: 1, + Error: 2, +} as const; + +type State = keyof typeof State; + +export function AsyncWidget(p: { + loadKey: unknown; + load: () => Promise; + errMsg: string; + build: () => React.ReactElement; + ready?: boolean; + errAdditionalElement?: () => React.ReactElement; +}): React.ReactElement { + const [state, setState] = React.useState(State.Loading); + + const load = async () => { + try { + setState(State.Loading); + await p.load(); + setState(State.Ready); + } catch (e) { + console.error(e); + setState(State.Error); + } + }; + + React.useEffect(() => { + load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [p.loadKey]); + + if (state === State.Error) + return ( + + + {p.errMsg} + + + + + {p.errAdditionalElement?.()} + + ); + + if (state === State.Loading || p.ready === false) + return ( + + + + ); + + return p.build(); +} 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/EmojiIcon.tsx b/matrixgw_frontend/src/widgets/EmojiIcon.tsx new file mode 100644 index 0000000..c0a3c64 --- /dev/null +++ b/matrixgw_frontend/src/widgets/EmojiIcon.tsx @@ -0,0 +1,31 @@ +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; + size?: number; +}): React.ReactElement { + const unified = emojiUnicode(p.emojiKey); + return ( + + ); +} 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..500476f --- /dev/null +++ b/matrixgw_frontend/src/widgets/NotLinkedAccountMessage.tsx @@ -0,0 +1,26 @@ +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! +
+ + + +
+ ); +} 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/TimeWidget.tsx b/matrixgw_frontend/src/widgets/TimeWidget.tsx new file mode 100644 index 0000000..1813af3 --- /dev/null +++ b/matrixgw_frontend/src/widgets/TimeWidget.tsx @@ -0,0 +1,31 @@ +import { Tooltip } from "@mui/material"; +import { + formatDateTime, + formatDate, + timeDiff, + timeDiffFromNow, +} from "../utils/DateUtils"; + +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)} + + + ); +} 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..4c34f6f --- /dev/null +++ b/matrixgw_frontend/src/widgets/auth/BaseLoginPage.tsx @@ -0,0 +1,66 @@ +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", + 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 ( + + + + MatrixGW + + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx new file mode 100644 index 0000000..81f706e --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/BaseAuthenticatedPage.tsx @@ -0,0 +1,153 @@ +import { Button } from "@mui/material"; +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +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"; + +interface UserInfoContext { + info: UserInfo; + reloadUserInfo: () => void; + signOut: () => void; +} + +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..."); + await loadUserInfo(); + } 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(); + + const signOut = () => { + AuthApi.SignOut(); + navigate("/"); + auth.setSignedIn(false); + }; + + const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] = + React.useState(false); + 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 ( + ( + <> + + + )} + build={() => ( + + + + + + + + + + + + )} + /> + ); +} + +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 new file mode 100644 index 0000000..f492e76 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardHeader.tsx @@ -0,0 +1,160 @@ +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"; +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 * 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"; + +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 { + menuOpen: boolean; + onToggleMenu: (open: boolean) => void; +} + +export default function DashboardHeader({ + menuOpen, + onToggleMenu, +}: DashboardHeaderProps) { + const user = useUserInfo(); + + 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)} + + + + + + + MatrixGW + + + + + + {/* User avatar */} + + + + + {user.info.name} + + + {user.info.email} + + + + + + + + + + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx new file mode 100644 index 0000000..2b1da24 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebar.tsx @@ -0,0 +1,207 @@ +import BugReportIcon from "@mui/icons-material/BugReport"; +import ForumIcon from "@mui/icons-material/Forum"; +import KeyIcon from "@mui/icons-material/Key"; +import LinkIcon from "@mui/icons-material/Link"; +import Box from "@mui/material/Box"; +import Drawer from "@mui/material/Drawer"; +import List from "@mui/material/List"; +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"; +import { DRAWER_WIDTH, MINI_DRAWER_WIDTH } from "./constants"; +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, + container, +}: DashboardSidebarProps) { + const theme = useTheme(); + const user = useUserInfo(); + + const isOverSmViewport = useMediaQuery(theme.breakpoints.up("sm")); + const isOverMdViewport = useMediaQuery(theme.breakpoints.up("md")); + + const [isFullyExpanded, setIsFullyExpanded] = 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]); + + const handleSetSidebarExpanded = React.useCallback( + (newExpanded: boolean) => () => { + setExpanded(newExpanded); + }, + [setExpanded] + ); + + const handlePageItemClick = React.useCallback(() => { + if (!isOverSmViewport) { + setExpanded(false); + } + }, [setExpanded, isOverSmViewport]); + + const hasDrawerTransitions = isOverSmViewport && isOverMdViewport; + + const getDrawerContent = React.useCallback( + (viewport: "phone" | "desktop") => ( + + + + } + href="/" + mini={viewport === "desktop"} + /> + + } + href="/matrix_link" + mini={viewport === "desktop"} + /> + } + href="/tokens" + mini={viewport === "desktop"} + /> + } + href="/wsdebug" + mini={viewport === "desktop"} + /> + + + + ), + [ + expanded, + hasDrawerTransitions, + isFullyExpanded, + user.info.matrix_account_connected, + ] + ); + + const getDrawerSharedSx = React.useCallback( + (isTemporary: boolean, desktop?: boolean) => { + const drawerWidth = desktop + ? expanded + ? MINI_DRAWER_WIDTH + : 0 + : !expanded + ? 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] + ); + + const sidebarContextValue = React.useMemo(() => { + return { + onPageItemClick: handlePageItemClick, + fullyExpanded: isFullyExpanded, + hasDrawerTransitions, + }; + }, [handlePageItemClick, isFullyExpanded, hasDrawerTransitions]); + + return ( + + + {getDrawerContent("phone")} + + + {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..ef1e8ee --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarContext.tsx @@ -0,0 +1,9 @@ +import * as React from "react"; + +const DashboardSidebarContext = React.createContext<{ + onPageItemClick: () => void; + fullyExpanded: boolean; + hasDrawerTransitions: boolean; +} | null>(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/DashboardSidebarPageItem.tsx b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx new file mode 100644 index 0000000..ef83b58 --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/DashboardSidebarPageItem.tsx @@ -0,0 +1,140 @@ +import Avatar from "@mui/material/Avatar"; +import Box from "@mui/material/Box"; +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 Typography from "@mui/material/Typography"; +import type {} from "@mui/material/themeCssVarsAugmentation"; +import * as React from "react"; +import { Link, matchPath, useLocation } from "react-router"; +import DashboardSidebarContext from "./DashboardSidebarContext"; +import { MINI_DRAWER_WIDTH } from "./constants"; + +export interface DashboardSidebarPageItemProps { + title: string; + icon?: React.ReactNode; + href: string; + action?: React.ReactNode; + disabled?: boolean; + mini?: boolean; +} + +export default function DashboardSidebarPageItem({ + title, + icon, + href, + action, + disabled = false, + mini = false, +}: DashboardSidebarPageItemProps) { + const { pathname } = useLocation(); + + const sidebarContext = React.useContext(DashboardSidebarContext); + if (!sidebarContext) { + throw new Error("Sidebar context was used without a provider."); + } + const { onPageItemClick, fullyExpanded = true } = sidebarContext; + + const hasExternalHref = href + ? href.startsWith("http://") || href.startsWith("https://") + : false; + + const LinkComponent = hasExternalHref ? "a" : Link; + + const selected = !!matchPath(href, pathname); + + return ( + + + + {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} + + + + ); +} 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..aa2d29d --- /dev/null +++ b/matrixgw_frontend/src/widgets/dashboard/ThemeSwitcher.tsx @@ -0,0 +1,64 @@ +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 function useActualColorMode(): { + mode: "light" | "dark"; + setMode: (mode: "light" | "dark") => void; +} { + const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); + const preferredMode = prefersDarkMode ? "dark" : "light"; + + const { mode, setMode } = useColorScheme(); + + 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(mode === "dark" ? "light" : "dark"); + }, [mode, setMode]); + + 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", + }; +} 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} + /> + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx b/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx new file mode 100644 index 0000000..e4fb748 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/AccountIcon.tsx @@ -0,0 +1,19 @@ +import { Avatar } from "@mui/material"; +import type { UserProfile } from "../../api/matrix/MatrixApiProfile"; +import { MatrixApiMedia } from "../../api/matrix/MatrixApiMedia"; + +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/AppIconModifier.tsx b/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx new file mode 100644 index 0000000..11673e9 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/AppIconModifier.tsx @@ -0,0 +1,35 @@ +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[] { + 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; +} + +const iconPath = getInitialFavicon()[0].getAttribute("href")!; + +export function AppIconModifier(p: { + numberUnread: number; + state: string; +}): React.ReactElement { + 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 new file mode 100644 index 0000000..9437353 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/MainMessagesWidget.tsx @@ -0,0 +1,196 @@ +import { Divider } from "@mui/material"; +import React from "react"; +import { MatrixApiEvent } from "../../api/matrix/MatrixApiEvent"; +import { + MatrixApiProfile, + type UsersMap, +} from "../../api/matrix/MatrixApiProfile"; +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 { AppIconModifier } from "./AppIconModifier"; +import { MatrixWS } from "./MatrixWS"; +import { RoomSelector } from "./RoomSelector"; +import { RoomWidget } from "./RoomWidget"; +import { SpaceSelector } from "./SpaceSelector"; + +export function MainMessageWidget(): React.ReactElement { + const [rooms, setRooms] = React.useState(); + const [users, setUsers] = React.useState(); + + const loadRoomsList = 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 ( + ( + setRooms((r) => cb(r!))} + /> + )} + /> + ); +} + +function MainMessageWidgetInner(p: { + rooms: Room[]; + users: UsersMap; + onRoomsListUpdate: (cb: (a: Room[]) => Room[]) => void; +}): React.ReactElement { + const user = useUserInfo(); + + const [space, setSpace] = React.useState(); + const [currentRoom, setCurrentRoom] = 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]); + + const unreadRooms = React.useMemo( + () => + p.rooms.filter( + (r) => r.number_unread_messages > 0 && r.notifications === "AllMessages" + ).length, + [p.rooms] + ); + + const setRefreshCount = React.useState(0)[1]; + 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 receipts = await MatrixApiRoom.RoomReceipts(currentRoom); + const mgr = new RoomEventsManager(currentRoom!, messages, receipts); + setRoomMgr(mgr); + }; + + const [wsState, setWsState] = React.useState(""); + 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].notifications === "AllMessages") + n[idx] = { + ...n[idx], + number_unread_messages: n[idx].number_unread_messages + 1, + }; + 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 ( +
    + {/* Websocket */} +
    + +
    + + {/** Application icon modifier */} + + + {/* Space selector */} + + + {/* Separator */} + + + {/* Room selector */} + + + {/* Separator */} + + + {/* If no room is selected */} + {currentRoom === undefined && ( +
    + No room selected. +
    + )} + + {/* In case of room */} + {currentRoom && ( + ( + + )} + /> + )} +
    + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx new file mode 100644 index 0000000..ea9061a --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/MatrixWS.tsx @@ -0,0 +1,93 @@ +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"; + +export const WSState = { + Closed: "Closed", + Connected: "Connected", + Error: "Error", +} as const; + +export function MatrixWS(p: { + onMessage: (msg: WsMessage) => void; + onStateChange?: (state: string) => void; +}): 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]); + + // 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); + + React.useEffect(() => { + const id = Math.random(); + const ws = new WebSocket(WsApi.WsURL); + wsId.current = id; + + // 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(WSState.Error); + stateCbRef.current?.(WSState.Error); + + setTimeout(() => setConnCount(connCount + 1), 500); + }; + + // Close + ws.onclose = () => { + if (wsId.current !== id) return; + setState(WSState.Closed); + stateCbRef.current?.(WSState.Closed); + wsId.current = undefined; + }; + + // Message + ws.onmessage = (msg) => { + if (wsId.current !== id) return; + + const dec = JSON.parse(msg.data); + console.info("WS message", dec); + cbRef.current(dec); + }; + + return () => ws.close(); + }, [connCount, snackbar]); + + return ( + + + + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx b/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx new file mode 100644 index 0000000..8efafac --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/RoomIcon.tsx @@ -0,0 +1,33 @@ +import { Avatar } from "@mui/material"; +import { MatrixApiMedia } from "../../api/matrix/MatrixApiMedia"; +import type { UsersMap } from "../../api/matrix/MatrixApiProfile"; +import { + mainRoomMember, + roomName, + type Room, +} from "../../api/matrix/MatrixApiRoom"; +import { useUserInfo } from "../dashboard/BaseAuthenticatedPage"; + +export function RoomIcon(p: { + room: Room; + users: UsersMap; +}): React.ReactElement { + const user = useUserInfo(); + + let url = p.room.avatar; + + 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); + + return ( + + {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..3fc340a --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/RoomMessagesList.tsx @@ -0,0 +1,572 @@ +import AddReactionIcon from "@mui/icons-material/AddReaction"; +import DeleteIcon from "@mui/icons-material/Delete"; +import DownloadIcon from "@mui/icons-material/Download"; +import EditIcon from "@mui/icons-material/Edit"; +import { + Box, + Button, + ButtonGroup, + Chip, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + TextField, + Tooltip, + 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"; +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"; +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: { + room: Room; + users: UsersMap; + manager: RoomEventsManager; +}): React.ReactElement { + const snackbar = useSnackbar(); + + const [loadingOlder, setLoadingOlder] = React.useState(false); + + const listContainerRef = React.createRef(); + 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" }); + }, [lastEventId, messagesEndRef]); + + // Watch scroll to detect when user reach the top to load older messages + const handleScroll = async () => { + if (!listContainerRef.current || loadingOlder || !p.manager.canLoadOlder) + return; + + const { scrollTop } = listContainerRef.current; + + if (scrollTop !== 0) { + return; + } + + 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 ( +
    + {/* Empty conversation notice */} + {p.manager.messages.length === 0 && ( +
    + No message in this conversation yet! +
    + )} + + {/** Begining of conversation */} + {!p.manager.canLoadOlder && ( + + Begining of conversation + + )} + + {/** Loading older messages spinner */} + {loadingOlder && ( +
    + +
    + )} + + {p.manager.messages.map((m, idx) => ( + 0 && + 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.manager.messages[idx - 1].time_sent_dayjs.startOf("day").unix() + } + receipts={p.manager.receiptsEventsMap.get(m.event_id)} + /> + ))} + +
    +
    + ); +} + +function RoomMessage(p: { + room: Room; + users: UsersMap; + message: Message; + previousFromSamePerson: boolean; + firstMessageOfDay: boolean; + receipts?: Receipt[]; +}): React.ReactElement { + const theme = useTheme(); + const user = useUserInfo(); + const alert = useAlert(); + const confirm = useConfirm(); + const snackbar = useSnackbar(); + const loadingMessage = useLoadingMessage(); + + const [showImageFullScreen, setShowImageFullScreen] = React.useState(false); + + const [editMessage, setEditMessage] = React.useState(); + const [pickReaction, setPickReaction] = React.useState(false); + + const closeImageFullScreen = () => setShowImageFullScreen(false); + + 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}`); + } + }; + + 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(); + } + }; + + 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(); + } + }; + + const handleToggleReaction = async ( + key: string, + reaction: MessageReaction | undefined + ) => { + 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 */} + {p.firstMessageOfDay && ( + + {p.message.time_sent_dayjs.format("DD/MM/YYYY")} + + )} + + {/* Give person name if required */} + {(!p.previousFromSamePerson || p.firstMessageOfDay) && sender && ( +
    + +     + {sender.display_name} +
    + )} + + {/* Message content */} + + +   {p.message.time_sent_dayjs.format("HH:mm")} + + + {/** Message itself */} +
    + {/* Image */} + {p.message.type === "m.image" && ( + setShowImageFullScreen(true)} + src={MatrixApiEvent.GetEventFileURL( + p.room, + p.message.event_id, + true + )} + style={{ + maxWidth: "200px", + }} + /> + )} + + {/* 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}
    + )} +
    + + {/* 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 */} + + {/* Common reactions */} + {/* 👍 */} + {/* ♥️ */} + {/* 😂 */} + {/* Add reaction */} + + {/* Edit text message */} + {p.message.account === user.info.matrix_user_id && + !p.message.file && ( + + )} + {/* Delete message */} + {p.message.account === user.info.matrix_user_id && ( + + )} + +
    + + {/* Reactions */} + + {[...p.message.reactions.keys()].map((r) => { + const reactions = p.message.reactions.get(r)!; + const userReaction = reactions.find( + (r) => r.account === user.info.matrix_user_id + ); + return ( + + {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}
    +
    + } + /> +
    + ); + })} +
    + + {/* Full screen image dialog */} + + + + + {/* Pick reaction dialog */} + + handleSelectEmoji(emoji.emoji)} + /> + + + {/* Edit message dialog */} + + Edit message content + + Enter new message content: +
    + setEditMessage(e.target.value)} + /> + +
    + + + + +
    + + ); +} + +function ReactionButton(p: { + room: Room; + message: Message; + emojiKey: string; +}): React.ReactElement { + const alert = useAlert(); + const user = useUserInfo(); + + const sendEmoji = async () => { + try { + await MatrixApiEvent.ReactToEvent(p.room, p.message.event_id, p.emojiKey); + } catch (e) { + console.error("Failed to send reaction!", e); + alert(`Failed to send reaction! ${e}`); + } + }; + + // Do not offer to react to existing reactions + if ( + p.message.reactions + .get(p.emojiKey) + ?.find( + (r) => r.key === p.emojiKey && r.account === user.info.matrix_user_id + ) !== undefined + ) + return <>; + + return ( + + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx b/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx new file mode 100644 index 0000000..258402e --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/RoomSelector.tsx @@ -0,0 +1,105 @@ +import { + Chip, + List, + ListItem, + ListItemButton, + 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"; +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(); + + 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 ( +
    + No room to display. +
    + ); + + return ( +
    + {/** Chip bar */} +
    + setUnread(!unread)} style={{ cursor: "pointer" }}> + + +
    + + {/** Rooms list */} + + {shownRooms.map((r) => ( + + ) + } + disablePadding + > + p.onChange(r)} + dense + selected={p.currRoom?.id === r.id} + > + + + + 0 ? "bold" : undefined, + }} + > + {roomName(user.info, r, p.users)} + + } + /> + + + ))} + +
    + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx new file mode 100644 index 0000000..5dce7ea --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/RoomWidget.tsx @@ -0,0 +1,44 @@ +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 { TypingNotice } from "./TypingNotice"; + +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 ( +
    + + + +
    + ); +} 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)} + /> + + + +
    +
    + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx b/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx new file mode 100644 index 0000000..1557b16 --- /dev/null +++ b/matrixgw_frontend/src/widgets/messages/SpaceSelector.tsx @@ -0,0 +1,56 @@ +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] + ); + + // Do not display space bar if your is not member of any space + if (spaces.length === 0) return <>; + + 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 ( + + ); +} diff --git a/matrixgw_frontend/src/widgets/messages/TypingNotice.tsx b/matrixgw_frontend/src/widgets/messages/TypingNotice.tsx new file mode 100644 index 0000000..578718d --- /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, p.users, user.info.matrix_user_id] + ); + + if (users.length === 0) return <>; + + return ( + + {users.map((u) => u.display_name ?? u.display_name).join(", ")}{" "} + {users.length > 1 ? "are" : "is"} typing... + + ); +} 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/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/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