diff --git a/.env b/.env new file mode 100644 index 000000000..b90cc77ee --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +JWT_SECRET=fUkwAovmPVsfEFx8Eu9rGRIawGPhdOSw2hsWohXW1Osxcy4in7Lad97CR08bA775XJ2Li+DD5xL09KaaA8cNuQ== +DATABASE_URL=postgres://postgres:123zxcQWE@localhost:5432 +POSTGRES_PASSWORD=123zxcQWE +SQLX_OFFLINE=true diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index bce2742eb..b433ddc15 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -6,93 +6,119 @@ on: branches: - main workflow_dispatch: +env: + SQLX_OFFLINE: true jobs: build: runs-on: ubuntu-latest - + environment: DOCKER-Sprint1 + + services: + postgres: + # Docker Hub image + image: postgres:15.2-alpine + # Environment variables scoped only for the `postgres` element + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + POSTGRES_DB: postgres + # Opens tcp port 5432 on the host and service container + ports: + - 5432:5432 + redis: + image: redis:7.0-alpine + ports: + - 6379:6379 steps: # Checkout code from the repository - - name: Checkout code - uses: actions/checkout@v2 - - # Cache dependencies to speed up build times - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: | - app-service/.cargo - app-service/target/ - auth-service/.cargo - auth-service/target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- - - - name: Install Rust - run: rustup update stable && rustup default stable - - - name: Build and test app-service code - working-directory: ./app-service - run: | - cargo build --verbose - cargo test --verbose - - - name: Build and test auth-service code - working-directory: ./auth-service - run: | - cargo build --verbose - cargo test --verbose - - # Set up Docker Buildx for multi-platform builds - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push Docker images - uses: docker/bake-action@v2.3.0 - with: - push: true - files: | - compose.yml - compose.override.yml - set: | - *.cache-from=type=gha - *.cache-to=type=gha,mode=max + - name: Checkout code + uses: actions/checkout@v2 + + # Cache dependencies to speed up build times + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + app-service/.cargo + app-service/target/ + auth-service/.cargo + auth-service/target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Install Rust + run: rustup update stable && rustup default stable + + - name: Build and test app-service code + working-directory: ./app-service + run: | + cargo build --verbose + cargo test --verbose + + - name: Build and test auth-service code + working-directory: ./auth-service + env: + JWT_SECRET: ${{ secrets.JWT_SECRET }} + run: | + export JWT_SECRET=secret + export POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} + cargo build --verbose + cargo test --verbose + + # Set up Docker Buildx for multi-platform builds + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push Docker images + uses: docker/bake-action@v2.3.0 + with: + push: true + files: | + compose.yml + compose.override.yml + set: | + *.cache-from=type=gha + *.cache-to=type=gha,mode=max deploy: needs: build runs-on: ubuntu-latest + environment: DOCKER-Sprint1 steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Log in to Docker Hub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Install sshpass - run: sudo apt-get install sshpass - - - name: Copy compose.yml to droplet - run: sshpass -v -p '${{ secrets.DROPLET_PASSWORD }}' scp -o StrictHostKeyChecking=no compose.yml root@${{ vars.DROPLET_IP }}:~ - - - name: Deploy - uses: appleboy/ssh-action@master - with: - host: ${{ vars.DROPLET_IP }} - username: root - password: ${{ secrets.DROPLET_PASSWORD }} - script: | - cd ~ - export AUTH_SERVICE_IP=${{ vars.DROPLET_IP }} - docker compose down - docker compose pull - docker compose up -d \ No newline at end of file + - name: Checkout code + uses: actions/checkout@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Install sshpass + run: sudo apt-get install sshpass + + - name: Copy compose.yml to droplet + run: sshpass -v -p '${{ secrets.DROPLET_PASSWORD }}' scp -o StrictHostKeyChecking=no compose.yml root@${{ vars.DROPLET_IP }}:~ + + - name: Deploy + uses: appleboy/ssh-action@master + with: + host: ${{ vars.DROPLET_IP }} + username: root + password: ${{ secrets.DROPLET_PASSWORD }} + script: | + cd ~ + export JWT_SECRET=${{ secrets.JWT_SECRET }} + export AUTH_SERVICE_IP=${{ vars.DROPLET_IP }} + export POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} + docker compose down + docker compose pull + docker compose up -d diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b323cc435 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/test_macros/target +.env diff --git a/README.md b/README.md index 6d390a25b..6d0eac133 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,67 @@ visit http://localhost:3000 ## Run servers locally (Docker) ```bash -docker compose build -docker compose up +./docker.sh ``` +```CMD for Windows +.\docker.bat +``` + +visit http://localhost:8000 and http://localhost:3000 + + + +## Database Migrations +```bash +#more info about sqlx-cli +https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md#install + +1. cargo install sqlx-cli --no-default-features --features native-tls,postgres + + +#NOTE: Make sure to execute this command in the auth-service directory! +# creates 2 new migration scripts named: +# "migrations/[TIMESTAMP]_create_users_table_down.sql" and "migrations/[TIMESTAMP]_create_users_table_up.sql" +2. sqlx migrate add -r create_users_table + +#NOTE: Make sure to execute this command in the auth-service directory! +#Generates build.rs script, which is used to generate the database schema +3. sqlx migrate build-script + +4. +a #SQLX_OFFLINE environment variable to avoid connecting to the database at runtime +#Add this line to Dockerfile +# # Build application +# COPY . . +ENV SQLX_OFFLINE=true +#RUN cargo build --release --bin auth-service -visit http://localhost:8000 and http://localhost:3000 \ No newline at end of file +b # Adn to .env file +SQLX_OFFLINE=true + +c#And execute this +cargo sqlx prepare + +d #Restart rust-analyzer sever +``` + +## Docker Postgres +```bash +# Pull the latest Postgres image from Docker Hub +1. docker pull postgres:15.2-alpine + +# Run the Postgres container +2. docker run --name ps-db -e POSTGRES_PASSWORD=[YOUR_POSTGRES_PASSWORD] -p 5432:5432 -d postgres:15.2-alpine +``` + + +## Drop Test Databases with Uiid as name +```sql +SELECT format( + 'DROP DATABASE "%s";', + datname +) +FROM pg_database +WHERE datname ~ +'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; +``` diff --git a/app-service/Dockerfile b/app-service/Dockerfile index 23441cc26..e06f453a8 100644 --- a/app-service/Dockerfile +++ b/app-service/Dockerfile @@ -1,5 +1,5 @@ # Start with image that has the Rust toolchain installed -FROM rust:1.90-alpine AS chef +FROM rust:1.91-alpine AS chef USER root # Add cargo-chef to cache dependencies RUN apk add --no-cache musl-dev & cargo install cargo-chef diff --git a/auth-service/.gitignore b/auth-service/.gitignore index 0b745e292..78ae7deff 100644 --- a/auth-service/.gitignore +++ b/auth-service/.gitignore @@ -1,2 +1,3 @@ /target -.env \ No newline at end of file +/test_macros/target +.env diff --git a/auth-service/.sqlx/query-2f716a7e7661b0e891cab523296b3fac3894c726a945d7dc8908ea317c86a7c4.json b/auth-service/.sqlx/query-2f716a7e7661b0e891cab523296b3fac3894c726a945d7dc8908ea317c86a7c4.json new file mode 100644 index 000000000..bb3789c55 --- /dev/null +++ b/auth-service/.sqlx/query-2f716a7e7661b0e891cab523296b3fac3894c726a945d7dc8908ea317c86a7c4.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT email, password_hash, requires_2fa FROM users WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "email", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "password_hash", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "requires_2fa", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "2f716a7e7661b0e891cab523296b3fac3894c726a945d7dc8908ea317c86a7c4" +} diff --git a/auth-service/.sqlx/query-7e78a365258ce2e8870ecde0be89768f8fb8d3a2e8b605749a58cdfd2aa5b0f2.json b/auth-service/.sqlx/query-7e78a365258ce2e8870ecde0be89768f8fb8d3a2e8b605749a58cdfd2aa5b0f2.json new file mode 100644 index 000000000..639f5cd98 --- /dev/null +++ b/auth-service/.sqlx/query-7e78a365258ce2e8870ecde0be89768f8fb8d3a2e8b605749a58cdfd2aa5b0f2.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO users (email, password_hash, requires_2fa) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "7e78a365258ce2e8870ecde0be89768f8fb8d3a2e8b605749a58cdfd2aa5b0f2" +} diff --git a/auth-service/Cargo.lock b/auth-service/Cargo.lock index cd9bd4144..0df4243e4 100644 --- a/auth-service/Cargo.lock +++ b/auth-service/Cargo.lock @@ -2,6 +2,83 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -12,11 +89,41 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" name = "auth-service" version = "0.1.0" dependencies = [ + "argon2", + "async-trait", "axum", + "axum-extra", + "chrono", + "color-eyre", + "dotenvy", + "fake", + "jsonwebtoken", + "lazy_static", + "quickcheck", + "quickcheck_macros", + "rand 0.9.4", + "redis", + "reqwest", + "serde", + "serde_json", + "sqlx", + "test_macros", + "thiserror", "tokio", "tower-http", + "tracing", + "tracing-error", + "tracing-subscriber", + "uuid", + "validator", ] +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.8.6" @@ -24,13 +131,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ "axum-core", + "axum-macros", "bytes", "form_urlencoded", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.7.0", "hyper-util", "itoa", "matchit", @@ -42,7 +150,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower", "tower-layer", @@ -58,655 +166,3484 @@ checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", - "sync_wrapper", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", ] [[package]] -name = "bitflags" -version = "2.10.0" +name = "axum-extra" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "5136e6c5e7e7978fe23e9876fb924af2c0f84c72127ac6ac17e7c46f457d362c" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie 0.18.1", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] [[package]] -name = "bytes" -version = "1.10.1" +name = "axum-macros" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "cfg-if" -version = "1.0.4" +name = "backtrace" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] [[package]] -name = "fnv" -version = "1.0.7" +name = "base16ct" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "base64" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] -name = "futures-channel" -version = "0.3.31" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", -] +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "futures-core" -version = "0.3.31" +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] -name = "futures-sink" -version = "0.3.31" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "futures-task" -version = "0.3.31" +name = "bitflags" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] [[package]] -name = "futures-util" -version = "0.3.31" +name = "blake2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "pin-utils", + "digest", ] [[package]] -name = "http" -version = "1.3.1" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "bytes", - "fnv", - "itoa", + "generic-array", ] [[package]] -name = "http-body" -version = "1.0.1" +name = "bumpalo" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] -name = "http-body-util" -version = "0.1.3" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", + "find-msvc-tools", + "shlex", ] [[package]] -name = "http-range-header" -version = "0.4.2" +name = "cfg-if" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "httparse" -version = "1.10.1" +name = "chrono" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] [[package]] -name = "httpdate" -version = "1.0.3" +name = "color-eyre" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] [[package]] -name = "hyper" -version = "1.7.0" +name = "color-spantrace" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", ] [[package]] -name = "hyper-util" -version = "0.1.17" +name = "combine" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "futures-core", - "http", - "http-body", - "hyper", + "memchr", "pin-project-lite", "tokio", - "tower-service", + "tokio-util", ] [[package]] -name = "itoa" -version = "1.0.15" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] [[package]] -name = "libc" -version = "0.2.177" +name = "const-oid" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "lock_api" -version = "0.4.14" +name = "cookie" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" dependencies = [ - "scopeguard", + "percent-encoding", + "time", + "version_check", ] [[package]] -name = "log" -version = "0.4.28" +name = "cookie" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] [[package]] -name = "matchit" -version = "0.8.4" +name = "cookie_store" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie 0.17.0", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] [[package]] -name = "memchr" -version = "2.7.6" +name = "core-foundation" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "mime" -version = "0.3.17" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "mime_guess" -version = "2.0.5" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ - "mime", - "unicase", + "libc", ] [[package]] -name = "mio" -version = "1.1.0" +name = "crc" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", + "crc-catalog", ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "crc-catalog" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] -name = "parking_lot" -version = "0.12.5" +name = "crossbeam-queue" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" dependencies = [ - "lock_api", - "parking_lot_core", + "crossbeam-utils", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "crossbeam-utils" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", ] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] [[package]] -name = "proc-macro2" -version = "1.0.103" +name = "curve25519-dalek" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ - "unicode-ident", + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", ] [[package]] -name = "quote" -version = "1.0.41" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", + "quote", + "syn", ] [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "der" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "bitflags", + "const-oid", + "pem-rfc7468", + "zeroize", ] [[package]] -name = "ryu" -version = "1.0.20" +name = "deranged" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "deunicode" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" [[package]] -name = "serde" -version = "1.0.228" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "serde_core", + "block-buffer", + "const-oid", + "crypto-common", + "subtle", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "serde_derive", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "dotenvy" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "proc-macro2", - "quote", - "syn", + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] -name = "serde_json" -version = "1.0.145" +name = "ed25519" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", + "pkcs8", + "signature", ] [[package]] -name = "serde_path_to_error" -version = "0.1.20" +name = "ed25519-dalek" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "itoa", + "curve25519-dalek", + "ed25519", "serde", - "serde_core", + "sha2", + "subtle", + "zeroize", ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "either" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", "serde", ] [[package]] -name = "signal-hook-registry" -version = "1.4.6" +name = "elliptic-curve" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "libc", + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", ] [[package]] -name = "smallvec" -version = "1.15.1" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] [[package]] -name = "socket2" -version = "0.6.1" +name = "env_filter" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ - "libc", - "windows-sys 0.60.2", + "log", + "regex", ] [[package]] -name = "syn" -version = "2.0.108" +name = "env_logger" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "env_filter", + "log", ] [[package]] -name = "sync_wrapper" +name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "tokio" -version = "1.48.0" +name = "etcetera" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", + "cfg-if", + "home", + "windows-sys 0.48.0", ] [[package]] -name = "tokio-macros" -version = "2.6.0" +name = "event-listener" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ - "proc-macro2", - "quote", - "syn", + "concurrent-queue", + "parking", + "pin-project-lite", ] [[package]] -name = "tokio-util" -version = "0.7.17" +name = "eyre" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", + "indenter", + "once_cell", ] [[package]] -name = "tower" -version = "0.5.2" +name = "fake" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "f2b0902eb36fbab51c14eda1c186bda119fcff91e5e4e7fc2dd2077298197ce8" dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", + "deunicode", + "either", + "rand 0.9.4", ] [[package]] -name = "tower-http" -version = "0.6.6" +name = "ff" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "tower-layer" -version = "0.3.3" +name = "fiat-crypto" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] -name = "tower-service" -version = "0.3.3" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "tracing" -version = "0.1.41" +name = "flume" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ - "log", - "pin-project-lite", - "tracing-core", + "futures-core", + "futures-sink", + "spin", ] [[package]] -name = "tracing-core" -version = "0.1.34" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", -] +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "unicase" -version = "2.8.1" +name = "foldhash" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] -name = "unicode-ident" -version = "1.0.22" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "futures-channel" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] [[package]] -name = "windows-link" -version = "0.2.1" +name = "futures-core" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] -name = "windows-sys" -version = "0.60.2" +name = "futures-executor" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ - "windows-targets", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "futures-intrusive" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ - "windows-link", + "futures-core", + "lock_api", + "parking_lot", ] [[package]] -name = "windows-targets" -version = "0.53.5" +name = "futures-io" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" +name = "futures-sink" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" +name = "futures-task" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] -name = "windows_i686_gnu" -version = "0.53.1" +name = "futures-util" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] [[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" +name = "generic-array" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] [[package]] -name = "windows_i686_msvc" -version = "0.53.1" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[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 = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.7.0", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +dependencies = [ + "base64 0.22.1", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", + "js-sys", + "p256", + "p384", + "pem", + "rand 0.8.6", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags 2.10.0", + "libc", + "plain", + "redox_syscall 0.7.5", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna 1.1.0", + "psl-types", +] + +[[package]] +name = "quickcheck" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c589f335db0f6aaa168a7cd27b1fc6920f5e1470c804f814d9cd6e62a0f70b" +dependencies = [ + "env_logger", + "log", + "rand 0.10.1", +] + +[[package]] +name = "quickcheck_macros" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a28b8493dd664c8b171dd944da82d933f7d456b829bfb236738e1fe06c5ba4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redis" +version = "0.32.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "014cc767fefab6a3e798ca45112bccad9c6e0e218fbd49720042716c73cfef44" +dependencies = [ + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.6.1", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4666a1a60d8412eab19d94f6d13dcc9cea0a5ef4fdf6a5db306537413c661b1b" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "cookie 0.17.0", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.10.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.10.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "test_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna 1.1.0", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" +dependencies = [ + "idna 1.1.0", + "once_cell", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.10.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.10.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/auth-service/Cargo.toml b/auth-service/Cargo.toml index dbf7bee78..10648bca4 100644 --- a/auth-service/Cargo.toml +++ b/auth-service/Cargo.toml @@ -6,6 +6,41 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum = "0.8.6" +#axum = "0.8.6" +axum = { version = "0.8.6", features = ["macros"] } +axum-extra = { version = "0.12.1", features = ["cookie"] } #provides extra utilities for axum. The cookie feature enables the CookieJar extractor, which we will use to view & set cookies. tokio = { version = "1.48.0", features = ["full"] } -tower-http = { version = "0.6.6", features = ["fs"] } \ No newline at end of file +tower-http = { version = "0.6.6", features = ["fs", "cors", "trace"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" +uuid = { version = "1.18.1", features = ["v4", "serde"] } +async-trait = "0.1.89" +validator = "=0.20.0" +jsonwebtoken = {version="10.2.0", features=["rust_crypto"]} # is an easy-to-use JWT library for Rust. It allows us to create and decode JWTs in a strongly typed way. +chrono = "0.4.42" #is a date and time library for Rust. We will use it to calculate JWT expiry. + +dotenvy = "0.15.7" #loads environment variables from a .env file. +lazy_static = "1.5.0" #provides a macro for declaring lazily evaluated statics in Rust. + +rand = "0.9.2" + +sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "postgres", "migrate"] } + +argon2 = { version = "0.5.3", features = ["std"] } + +redis = { version = "0.32.7", features = ["tokio-comp"] } + +tracing = "0.1.44" #tower-http = { ..., features = ["trace"] } +thiserror = "2.0.17" +color-eyre = "0.6.5" +tracing-subscriber = { version = "0.3.20", features = ["registry", "env-filter"] } +tracing-error = "0.2.1" + + +[dev-dependencies] +reqwest = { version = "0.11.26", default-features = false, features = ["json", "cookies"] } +fake = "=4.4.0" +rand = "0.9.2" +quickcheck = "1.0.3" +quickcheck_macros = "1.1.0" +test_macros = { path = "./test_macros" } diff --git a/auth-service/Dockerfile b/auth-service/Dockerfile index 8dac2d5aa..06bcec2fe 100644 --- a/auth-service/Dockerfile +++ b/auth-service/Dockerfile @@ -1,21 +1,30 @@ # Start with image that has the Rust toolchain installed -FROM rust:1.90-alpine AS chef +FROM rust:1.91-alpine AS chef USER root # Add cargo-chef to cache dependencies -RUN apk add --no-cache musl-dev & cargo install cargo-chef +RUN apk add --no-cache musl-dev && cargo install cargo-chef WORKDIR /app FROM chef AS planner COPY . . + +#test +RUN find /app -name Cargo.toml +RUN ls -la /app +RUN ls -la /app/test_macros +#test + # Capture info needed to build dependencies RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json +COPY . . # Build dependencies - this is the caching Docker layer! RUN cargo chef cook --release --recipe-path recipe.json # Build application COPY . . +ENV SQLX_OFFLINE=true RUN cargo build --release --bin auth-service # We do not need the Rust toolchain to run the binary! @@ -24,4 +33,5 @@ FROM debian:buster-slim AS runtime WORKDIR /app COPY --from=builder /app/target/release/auth-service /usr/local/bin COPY --from=builder /app/assets /app/assets +ENV REDIS_HOST_NAME=redis ENTRYPOINT ["/usr/local/bin/auth-service"] diff --git a/auth-service/build.rs b/auth-service/build.rs new file mode 100644 index 000000000..d5068697c --- /dev/null +++ b/auth-service/build.rs @@ -0,0 +1,5 @@ +// generated by `sqlx migrate build-script` +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/auth-service/migrations/20260520095216_create_users_table.down.sql b/auth-service/migrations/20260520095216_create_users_table.down.sql new file mode 100644 index 000000000..c99ddcdc8 --- /dev/null +++ b/auth-service/migrations/20260520095216_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; diff --git a/auth-service/migrations/20260520095216_create_users_table.up.sql b/auth-service/migrations/20260520095216_create_users_table.up.sql new file mode 100644 index 000000000..19144747f --- /dev/null +++ b/auth-service/migrations/20260520095216_create_users_table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS users( + email TEXT NOT NULL PRIMARY KEY, + password_hash TEXT NOT NULL, + requires_2fa BOOLEAN NOT NULL DEFAULT FALSE +); diff --git a/auth-service/src/app_state.rs b/auth-service/src/app_state.rs new file mode 100644 index 000000000..9a4fc075f --- /dev/null +++ b/auth-service/src/app_state.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; +use tokio::sync::RwLock; + +use crate::domain::data_stores::{BannedTokenStore, TwoFACodeStore, UserStore}; + +pub type TwoFACodeStoreType = Arc>; +pub type UserStoreType = Arc>; +pub type BannedTokenStoreType = Arc>; +pub type EmailClientType = Arc>; + +#[derive(Clone)] +pub struct AppState { + pub user_store: UserStoreType, + pub banned_tokens_store: BannedTokenStoreType, + pub two_fa_code_store: TwoFACodeStoreType, + pub email_client: EmailClientType, +} + +impl AppState { + pub fn new( + user_store: UserStoreType, + banned_tokens_store: BannedTokenStoreType, + two_fa_code_store: TwoFACodeStoreType, + email_client: EmailClientType, + ) -> Self { + Self { + user_store, + banned_tokens_store, + two_fa_code_store, + email_client, + } + } +} diff --git a/auth-service/src/domain/data_stores.rs b/auth-service/src/domain/data_stores.rs new file mode 100644 index 000000000..53cd34405 --- /dev/null +++ b/auth-service/src/domain/data_stores.rs @@ -0,0 +1,144 @@ +use super::email::Email; + +use super::user::User; + +// #[derive(Debug, PartialEq)] +// pub enum UserStoreError { +// UserAlreadyExists, +// UserNotFound, +// InvalidCredentials, +// UnexpectedError, +// } +// +use color_eyre::eyre::{eyre, Report, Result}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum UserStoreError { + #[error("User already exists")] + UserAlreadyExists, + #[error("User not found")] + UserNotFound, + #[error("Invalid credentials")] + InvalidCredentials, + #[error("Unexpected error")] + UnexpectedError(#[source] Report), +} + +impl PartialEq for UserStoreError { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (Self::UserAlreadyExists, Self::UserAlreadyExists) + | (Self::UserNotFound, Self::UserNotFound) + | (Self::InvalidCredentials, Self::InvalidCredentials) + | (Self::UnexpectedError(_), Self::UnexpectedError(_)) + ) + } +} + +#[derive(Debug, Error)] +pub enum BannedTokenStoreError { + // MissingToken, + // InvalidToken, + #[error("Unexpected error")] + UnexpectedError(#[source] Report), +} + +#[async_trait::async_trait] +pub trait BannedTokenStore { + async fn add_token(&mut self, token: String) -> Result<(), BannedTokenStoreError>; + async fn contains_token(&self, token: &str) -> Result; +} + +#[async_trait::async_trait] +pub trait UserStore { + async fn add_user(&mut self, user: User) -> Result<(), UserStoreError>; + async fn get_user(&self, email: &Email) -> Result; + async fn validate_user(&self, email: &Email, raw_password: &str) -> Result<(), UserStoreError>; +} + +#[async_trait::async_trait] +pub trait TwoFACodeStore { + async fn add_code( + &mut self, + email: Email, + login_attempt_id: LoginAttemptId, + code: TwoFACode, + ) -> Result<(), TwoFACodeStoreError>; + async fn remove_code(&mut self, email: &Email) -> Result<(), TwoFACodeStoreError>; + async fn get_code( + &self, + email: &Email, + ) -> Result<(LoginAttemptId, TwoFACode), TwoFACodeStoreError>; +} + +#[derive(Debug, Error)] +pub enum TwoFACodeStoreError { + #[error("Login Attempt ID not found")] + LoginAttemptIdNotFound, + #[error("Unexpected error")] + UnexpectedError(#[source] Report), +} + +impl PartialEq for TwoFACodeStoreError { + fn eq(&self, other: &Self) -> bool { + matches!( + (self, other), + (Self::LoginAttemptIdNotFound, Self::LoginAttemptIdNotFound) + | (Self::UnexpectedError(_), Self::UnexpectedError(_)) + ) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LoginAttemptId(String); + +impl LoginAttemptId { + pub fn parse(id: String) -> Result { + // Use the `parse_str` function from the `uuid` crate to ensure `id` is a valid UUID + let parsed_id = + uuid::Uuid::parse_str(&id).map_err(|_| eyre!("Invalid login attempt id"))?; + Ok(Self(parsed_id.to_string())) + } +} + +impl Default for LoginAttemptId { + fn default() -> Self { + Self(uuid::Uuid::new_v4().to_string()) + } +} + +impl AsRef for LoginAttemptId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct TwoFACode(String); + +impl TwoFACode { + pub fn parse(code: String) -> Result { + // Ensure `code` is a valid 6-digit code + let code_as_u32 = code.parse::().map_err(|_| eyre!("Invalid 2FA code"))?; + + if (100_000..=999_999).contains(&code_as_u32) { + Ok(Self(code)) + } else { + Err(eyre!("Invalid 2FA code")) + } + } +} + +use rand::Rng; +impl Default for TwoFACode { + fn default() -> Self { + Self(rand::rng().random_range(100_000..=999_999).to_string()) + } +} +impl AsRef for TwoFACode { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/auth-service/src/domain/email.rs b/auth-service/src/domain/email.rs new file mode 100644 index 000000000..b593d3885 --- /dev/null +++ b/auth-service/src/domain/email.rs @@ -0,0 +1,66 @@ +use validator::ValidateEmail; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Email(String); + +impl Email { + pub fn parse(email: String) -> Result { + if email.validate_email() { + return Ok(Self(email)); + } + Err(format!("{} is not a valid email.", email)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl AsRef for Email { + fn as_ref(&self) -> &str { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::Email; + + use fake::faker::internet::en::SafeEmail; + use fake::Fake; + use quickcheck::Gen; + use rand::SeedableRng; + + #[test] + fn empty_string_is_rejected() { + let email = "".to_string(); + assert!(Email::parse(email).is_err()); + } + #[test] + fn email_missing_at_symbol_is_rejected() { + let email = "ursuladomain.com".to_string(); + assert!(Email::parse(email).is_err()); + } + #[test] + fn email_missing_subject_is_rejected() { + let email = "@domain.com".to_string(); + assert!(Email::parse(email).is_err()); + } + + #[derive(Debug, Clone)] + struct ValidEmailFixture(pub String); + + impl quickcheck::Arbitrary for ValidEmailFixture { + fn arbitrary(g: &mut Gen) -> Self { + let seed: u64 = g.size() as u64; + let mut rng = rand::rngs::SmallRng::seed_from_u64(seed); + let email = SafeEmail().fake_with_rng(&mut rng); + Self(email) + } + } + + #[quickcheck_macros::quickcheck] + fn valid_emails_are_parsed_successfully(valid_email: ValidEmailFixture) -> bool { + Email::parse(valid_email.0).is_ok() + } +} diff --git a/auth-service/src/domain/email_client.rs b/auth-service/src/domain/email_client.rs new file mode 100644 index 000000000..50fb5fb23 --- /dev/null +++ b/auth-service/src/domain/email_client.rs @@ -0,0 +1,23 @@ +use super::email::Email; + +use color_eyre::eyre::Report; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum EmailError { + // #[error("Invalid credentials")] + // InvalidCredentials, + #[error("Unexpected error")] + UnexpectedError(#[source] Report), +} + +// This trait represents the interface all concrete email clients should implement +#[async_trait::async_trait] +pub trait EmailClient { + async fn send_email( + &self, + recipient: &Email, + subject: &str, + content: &str, + ) -> Result<(), EmailError>; +} diff --git a/auth-service/src/domain/errors.rs b/auth-service/src/domain/errors.rs new file mode 100644 index 000000000..ba9da74b9 --- /dev/null +++ b/auth-service/src/domain/errors.rs @@ -0,0 +1,28 @@ +// #[derive(Debug)] +// pub enum AuthAPIError { +// UserAlreadyExists, +// InvalidCredentials, +// UnexpectedError, +// IncorrectCredentials, +// MissingToken, +// InvalidToken, +// } + +use color_eyre::eyre::Report; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AuthAPIError { + #[error("User already exists")] + UserAlreadyExists, + #[error("Invalid credentials")] + InvalidCredentials, + #[error("Incorrect credentials")] + IncorrectCredentials, + #[error("Missing token")] + MissingToken, + #[error("Invalid token")] + InvalidToken, + #[error("Unexpected error")] + UnexpectedError(#[source] Report), +} diff --git a/auth-service/src/domain/mod.rs b/auth-service/src/domain/mod.rs new file mode 100644 index 000000000..c991f101c --- /dev/null +++ b/auth-service/src/domain/mod.rs @@ -0,0 +1,11 @@ +pub mod data_stores; +pub mod email; +pub mod errors; +pub mod password; +pub mod user; + +pub mod email_client; + +//pub use errors::AuthAPIError; +pub use errors::*; +pub use user::User; diff --git a/auth-service/src/domain/password.rs b/auth-service/src/domain/password.rs new file mode 100644 index 000000000..a9874b8b8 --- /dev/null +++ b/auth-service/src/domain/password.rs @@ -0,0 +1,219 @@ +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Algorithm, Argon2, Params, PasswordHasher, PasswordVerifier, Version, +}; + +use crate::domain::data_stores::UserStoreError; +use color_eyre::eyre::{eyre, Result}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct HashedPassword(String); + +impl HashedPassword { + pub fn parse_password_hash(hash: String) -> Result { + let expected_password_hash = + argon2::password_hash::PasswordHash::new(&hash).map_err(|op| op.to_string())?; + Ok(HashedPassword(expected_password_hash.to_string())) + } + + #[tracing::instrument(name = "Verify raw password", skip_all)] + pub async fn verify_raw_password(&self, password_candidate: &str) -> Result<()> +// Result<(), Box> + { + let current_span: tracing::Span = tracing::Span::current(); + + let password_hash = self.as_ref().to_owned(); + let password_candidate = password_candidate.to_owned(); + + let result = tokio::task::spawn_blocking(move || + // -> Result<(), Box> + { + current_span.in_scope(|| { + let expected_password_hash = + argon2::password_hash::PasswordHash::new(&password_hash)?; + + Argon2::default() + .verify_password(password_candidate.as_bytes(), &expected_password_hash) + .map_err(|e| e.into()) + }) + }) + .await; + + result? + } + + #[tracing::instrument(name = "HashedPassword Parse", skip_all)] + pub async fn parse(pass: String) -> Result { + if is_valid_password(&pass) { + // match compute_password_hash(&pass).await { + // Ok(hashed) => return Ok(HashedPassword(hashed)), + // Err(e) => return Err(UserStoreError::UnexpectedError(e.into())), + // } + + let result = compute_password_hash(&pass) + .await + .map_err(|e| UserStoreError::UnexpectedError(e.into()))?; + + Ok(Self(result)) + } else { + Err(eyre!("Failed to parse string to a HashedPassword type")) + } + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +#[tracing::instrument(name = "Computing password hash", skip_all)] +async fn compute_password_hash(password: &str) -> Result +//Result> +{ + let current_span: tracing::Span = tracing::Span::current(); + + let password = password.to_owned(); + + let result = tokio::task::spawn_blocking(move || + //-> color_eyre::eyre::Result + //-> Result> + { + current_span.in_scope(|| { + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::new( + Algorithm::Argon2id, + Version::V0x13, + Params::new(15000, 2, 1, None)?, + ) + .hash_password(password.as_bytes(), &salt)? + .to_string(); + + Ok(password_hash) + //Err(Box::new(std::io::Error::other("oh no!")) as Box) + //Err(eyre!("oh no!")) + }) + }) + .await; + + result? +} + +impl AsRef for HashedPassword { + fn as_ref(&self) -> &str { + &self.0 + } +} + +fn is_valid_password(pass: &str) -> bool { + pass.len() > 7 +} + +#[cfg(test)] +mod tests { + use super::HashedPassword; // updated! + use argon2::{ + // new + password_hash::{rand_core::OsRng, SaltString}, + Algorithm, + Argon2, + Params, + PasswordHasher, + Version, + }; + use fake::faker::internet::en::Password as FakePassword; + use fake::Fake; + use quickcheck::Gen; + use rand::SeedableRng; + + // updated! + #[tokio::test] + async fn empty_string_is_rejected() { + let password = "".to_owned(); + + // updated! + assert!(HashedPassword::parse(password).await.is_err()); + } + + // updated! + #[tokio::test] + async fn string_less_than_8_characters_is_rejected() { + let password = "1234567".to_owned(); + // updated! + assert!(HashedPassword::parse(password).await.is_err()); + } + + // new + #[test] + fn can_parse_valid_argon2_hash() { + // Arrange - Create a valid Argon2 hash + let raw_password = "TestPassword123"; + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::new( + Algorithm::Argon2id, + Version::V0x13, + Params::new(15000, 2, 1, None).unwrap(), + ); + + let hash_string = argon2 + .hash_password(raw_password.as_bytes(), &salt) + .unwrap() + .to_string(); + + // Act + let hash_password = HashedPassword::parse_password_hash(hash_string.clone()).unwrap(); + + // Assert + assert_eq!(hash_password.as_ref(), hash_string.as_str()); + assert!(hash_password.as_ref().starts_with("$argon2id$v=19$")); + } + + // new + #[tokio::test] + async fn can_verify_raw_password() { + let raw_password = "TestPassword123"; + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::new( + Algorithm::Argon2id, + Version::V0x13, + Params::new(15000, 2, 1, None).unwrap(), + ); + + let hash_string = argon2 + .hash_password(raw_password.as_bytes(), &salt) + .unwrap() + .to_string(); + + let hash_password = HashedPassword::parse_password_hash(hash_string.clone()).unwrap(); + + assert_eq!(hash_password.as_ref(), hash_string.as_str()); + assert!(hash_password.as_ref().starts_with("$argon2id$v=19$")); + + // TODO: Use verify_raw_password to verify the password match + let result = HashedPassword::parse(raw_password.to_owned()) + .await + .unwrap() + .verify_raw_password(raw_password) + .await; + + assert!(result.is_ok()); + // TODO: Assert the verification succeeds assert_eq!(result, ()) + } + + #[derive(Debug, Clone)] + struct ValidPasswordFixture(pub String); + + impl quickcheck::Arbitrary for ValidPasswordFixture { + fn arbitrary(g: &mut Gen) -> Self { + let seed: u64 = g.size() as u64; + let mut rng = rand::rngs::SmallRng::seed_from_u64(seed); + let password = FakePassword(8..30).fake_with_rng(&mut rng); + Self(password) + } + } + + // updated! + #[tokio::test] + #[quickcheck_macros::quickcheck] + async fn valid_passwords_are_parsed_successfully(valid_password: ValidPasswordFixture) -> bool { + HashedPassword::parse(valid_password.0).await.is_ok() // updated! + } +} diff --git a/auth-service/src/domain/user.rs b/auth-service/src/domain/user.rs new file mode 100644 index 000000000..daced3184 --- /dev/null +++ b/auth-service/src/domain/user.rs @@ -0,0 +1,18 @@ +use crate::domain::{email::Email, password::HashedPassword}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct User { + pub email: Email, + pub password: HashedPassword, + pub requires_2fa: bool, +} + +impl User { + pub fn new(email: Email, password: HashedPassword, requires_2fa: bool) -> Self { + Self { + email, + password, + requires_2fa, + } + } +} diff --git a/auth-service/src/lib.rs b/auth-service/src/lib.rs new file mode 100644 index 000000000..5421441f0 --- /dev/null +++ b/auth-service/src/lib.rs @@ -0,0 +1,137 @@ +use std::error::Error; + +use axum::{ + http::{Method, StatusCode}, + response::{IntoResponse, Response}, + routing::post, + Json, Router, +}; + +use serde::{Deserialize, Serialize}; +use tower_http::{cors::CorsLayer, services::ServeDir}; + +pub mod routes; +use routes::*; + +pub mod app_state; + +pub mod services; + +pub mod domain; +use domain::errors::AuthAPIError; + +pub mod utils; + +// This struct encapsulates our application-related logic. +pub struct Application { + server: axum::serve::Serve, + pub address: String, +} + +impl Application { + pub async fn build( + app_state: app_state::AppState, + address: &str, + ) -> Result> { + let router = Router::new() + .fallback_service(ServeDir::new("assets")) + .route("/signup", post(signup)) + .route("/login", post(login)) + .route("/logout", post(logout)) + .route("/verify-2fa", post(verify_2fa)) + .route("/verify-token", post(verify_token)) + .with_state(app_state) + .layer(Self::get_cors()?) + .layer( + // New! + // Add a TraceLayer for HTTP requests to enable detailed tracing + // This layer will create spans for each request using the make_span_with_request_id function, + // and log events at the start and end of each request using on_request and on_response functions. + tower_http::trace::TraceLayer::new_for_http() + .make_span_with(utils::tracing::make_span_with_request_id) + .on_request(utils::tracing::on_request) + .on_response(utils::tracing::on_response), + ); + + let listener = tokio::net::TcpListener::bind(address).await?; + let address = listener.local_addr()?.to_string(); + let server = axum::serve(listener, router); + + Ok(Self { address, server }) + } + + pub async fn run(self) -> Result<(), std::io::Error> { + tracing::info!("listening on {}", &self.address); + self.server.await + } + + fn get_cors() -> Result> { + let allowed_origins = [ + "http://localhost:8000".parse()?, + "http://157.230.221.162:8000".parse()?, + ]; + + Ok(CorsLayer::new() + // Allow GET and POST requests + .allow_methods([Method::GET, Method::POST]) + // Allow cookies to be included in requests + .allow_credentials(true) + .allow_origin(allowed_origins)) + } +} + +#[derive(Serialize, Deserialize)] +pub struct ErrorResponse { + pub error: String, +} + +impl IntoResponse for AuthAPIError { + fn into_response(self) -> Response { + log_error_chain(&self); + + let (status, error_message) = match self { + AuthAPIError::UserAlreadyExists => (StatusCode::CONFLICT, "User already exists"), + AuthAPIError::InvalidCredentials => (StatusCode::BAD_REQUEST, "Invalid credentials"), + AuthAPIError::IncorrectCredentials => { + (StatusCode::UNAUTHORIZED, "Incorect credentials") + } + AuthAPIError::UnexpectedError(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, "Unexpected error") + } + AuthAPIError::MissingToken => (StatusCode::BAD_REQUEST, "Token required"), + AuthAPIError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token"), + }; + let body = Json(ErrorResponse { + error: error_message.to_string(), + }); + (status, body).into_response() + } +} + +/* Helper functions*/ +pub async fn get_postgres_pool(url: &str) -> Result { + // Create a new PostgreSQL connection pool + sqlx::postgres::PgPoolOptions::new() + .max_connections(5) + .connect(url) + .await +} + +pub fn get_redis_client(redis_hostname: String) -> redis::RedisResult { + let redis_url = format!("redis://{}/", redis_hostname); + redis::Client::open(redis_url) +} + +fn log_error_chain(e: &(dyn Error + 'static)) { + let separator = + "\n-----------------------------------------------------------------------------------\n"; + let mut report = format!("{}{:?}\n", separator, e); + let mut current = e.source(); + while let Some(cause) = current { + let str = format!("Caused by:\n\n{:?}", cause); + report = format!("{}\n{}", report, str); + current = cause.source(); + } + report = format!("{}\n{}", report, separator); + tracing::error!("{}", report); +} diff --git a/auth-service/src/main.rs b/auth-service/src/main.rs index 1f243bd26..93cd947c8 100644 --- a/auth-service/src/main.rs +++ b/auth-service/src/main.rs @@ -1,23 +1,71 @@ -use axum::{response::Html, routing::get, serve, Router}; -use tower_http::services::ServeDir; +// mod services; +// mod app_state; + +use auth_service::app_state::AppState; +// use auth_service::services::data_stores::banned_tokens_store::HashsetBannedTokenStore; +// use auth_service::services::data_stores::hashmap_user_store::HashmapUserStore; +// use auth_service::services::data_stores::hashmap_two_fa_code_store::HashmapTwoFACodeStore; +use auth_service::services::data_stores::postgres_user_store::PostgresUserStore; +use auth_service::services::data_stores::redis_two_fa_code_store::RedisTwoFACodeStore; + +use auth_service::services::data_stores::redis_banned_token_store::RedisBannedTokenStore; +use auth_service::services::mock_email_client::MockEmailClient; +use auth_service::utils; +use auth_service::utils::tracing::init_tracing; #[tokio::main] async fn main() { - let assets_dir = ServeDir::new("assets"); - let app = Router::new() - .fallback_service(assets_dir) - .route("/hello", get(hello_handler)); - - // Here we are using ip 0.0.0.0 so the service is listening on all the configured network interfaces. - // This is needed for Docker to work, which we will add later on. - // See: https://stackoverflow.com/questions/39525820/docker-port-forwarding-not-working - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); - println!("listening on {}", listener.local_addr().unwrap()); - - axum::serve(listener, app).await.unwrap(); + color_eyre::install().expect("Failed to install color_eyre"); + init_tracing().expect("Failed to initialize tracing"); + //let user_store = std::sync::Arc::new(tokio::sync::RwLock::new(HashmapUserStore::default())); + //let banned_tokens_store =std::sync::Arc::new(tokio::sync::RwLock::new(HashsetBannedTokenStore::default())); + //let two_fa_code_store = std::sync::Arc::new(tokio::sync::RwLock::new(HashmapTwoFACodeStore::default())); + // + let redis_conn = std::sync::Arc::new(tokio::sync::RwLock::new(configure_redis())); + let banned_tokens_store = std::sync::Arc::new(tokio::sync::RwLock::new( + RedisBannedTokenStore::new(redis_conn.clone()), + )); + let two_fa_code_store = std::sync::Arc::new(tokio::sync::RwLock::new( + RedisTwoFACodeStore::new(redis_conn), + )); + + let email_client = std::sync::Arc::new(tokio::sync::RwLock::new(MockEmailClient)); + + let pg_pool = configure_postgresql().await; + let user_store = std::sync::Arc::new(tokio::sync::RwLock::new(PostgresUserStore::new(pg_pool))); + + let app_state = AppState::new( + user_store, + banned_tokens_store, + two_fa_code_store, + email_client, + ); + + let app = auth_service::Application::build(app_state, utils::constants::prod::APP_ADDRESS) + .await + .expect("Failed to build app"); + + app.run().await.expect("Failed to run app"); +} + +pub async fn configure_postgresql() -> sqlx::postgres::PgPool { + // Create a new database connection pool + let pg_pool = auth_service::get_postgres_pool(&utils::constants::DATABASE_URL) + .await + .expect("Failed to create Postgres connection pool!"); + + // Run database migrations against our test database! + sqlx::migrate!() + .run(&pg_pool) + .await + .expect("Failed to run migrations"); + + pg_pool } -async fn hello_handler() -> Html<&'static str> { - // TODO: Update this to a custom message! - Html("

Hello, World!

") +fn configure_redis() -> redis::Connection { + auth_service::get_redis_client(utils::constants::REDIS_HOST_NAME.to_owned()) + .expect("Failed to get Redis client") + .get_connection() + .expect("Failed to get Redis connection") } diff --git a/auth-service/src/routes/login.rs b/auth-service/src/routes/login.rs new file mode 100644 index 000000000..be32469ff --- /dev/null +++ b/auth-service/src/routes/login.rs @@ -0,0 +1,157 @@ +use crate::{ + app_state::AppState, + domain::{ + data_stores::{LoginAttemptId, TwoFACode}, + email::Email, + password::HashedPassword, + }, + utils, AuthAPIError, +}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use axum_extra::extract::CookieJar; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize)] +pub struct LoginRequest { + pub email: String, + pub password: String, +} + +impl LoginRequest { + pub fn new(email: &str, password: &str) -> Self { + LoginRequest { + email: email.to_owned(), + password: password.to_owned(), + } + } +} + +//#[axum::debug_handler] +pub async fn login( + State(state): State, + jar: CookieJar, + Json(request): Json, +) -> (CookieJar, Result) { + let email = match Email::parse(request.email) { + Ok(email) => email, + Err(_) => return (jar, Err(AuthAPIError::InvalidCredentials)), + }; + + if HashedPassword::parse(request.password.clone()) + .await + .is_err() + { + return (jar, Err(AuthAPIError::InvalidCredentials)); + } + + let user_store = state.user_store.write().await; + + if user_store + .validate_user(&email, &request.password) + .await + .is_err() + { + return (jar, Err(AuthAPIError::IncorrectCredentials)); + } + + let user = match user_store.get_user(&email).await { + Ok(user) => user, + Err(_) => return (jar, Err(AuthAPIError::IncorrectCredentials)), + }; + + // Handle request based on user's 2FA configuration + match user.requires_2fa { + true => handle_2fa(&state, &user.email, jar.clone()).await, + false => handle_no_2fa(&user.email, jar.clone()).await, + } +} + +// New! +async fn handle_2fa( + state: &AppState, + email: &Email, + jar: CookieJar, +) -> ( + CookieJar, + Result<(StatusCode, Json), AuthAPIError>, +) { + let login_attempt_id = LoginAttemptId::default(); + let two_fa_code = TwoFACode::default(); + + if let Err(e) = state + .two_fa_code_store + .write() + .await + .add_code(email.clone(), login_attempt_id.clone(), two_fa_code.clone()) + .await + { + return (jar, Err(AuthAPIError::UnexpectedError(e.into()))); + } + + let email_2fa_result = state + .email_client + .read() + .await + .send_email(email, "2FA Code", two_fa_code.as_ref()) + .await; + + if let Err(e) = email_2fa_result { + return (jar, Err(AuthAPIError::UnexpectedError(e.into()))); + } + + let auth_cookie = match utils::auth::generate_auth_cookie(email) { + Ok(cookie) => cookie, + Err(e) => return (jar, Err(AuthAPIError::UnexpectedError(e.into()))), + }; + + let updated_jar = jar.add(auth_cookie); + + ( + updated_jar, + Ok(( + StatusCode::PARTIAL_CONTENT, + Json::from(LoginResponse::TwoFactorAuth(TwoFactorAuthResponse { + login_attempt_id: login_attempt_id.as_ref().to_owned(), + message: "2FA required".to_owned(), + })), + )), + ) +} + +// New! +async fn handle_no_2fa( + email: &Email, + jar: CookieJar, +) -> ( + CookieJar, + Result<(StatusCode, Json), AuthAPIError>, +) { + let auth_cookie = match utils::auth::generate_auth_cookie(email) { + Ok(cookie) => cookie, + Err(e) => return (jar, Err(AuthAPIError::UnexpectedError(e.into()))), + }; + + let updated_jar = jar.add(auth_cookie); + + ( + updated_jar, + Ok((StatusCode::OK, Json::from(LoginResponse::RegularAuth))), + ) +} + +// The login route can return 2 possible success responses. +// This enum models each response! +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum LoginResponse { + RegularAuth, + TwoFactorAuth(TwoFactorAuthResponse), +} + +// If a user requires 2FA, this JSON body should be returned! +#[derive(Debug, Serialize, Deserialize)] +pub struct TwoFactorAuthResponse { + pub message: String, + #[serde(rename = "loginAttemptId")] + pub login_attempt_id: String, +} diff --git a/auth-service/src/routes/logout.rs b/auth-service/src/routes/logout.rs new file mode 100644 index 000000000..3f0ca8f87 --- /dev/null +++ b/auth-service/src/routes/logout.rs @@ -0,0 +1,41 @@ +use axum::{extract::State, http::StatusCode, response::IntoResponse}; +use axum_extra::extract::{cookie, CookieJar}; + +use crate::{ + app_state::AppState, + domain::AuthAPIError, + utils::{auth::validate_token, constants::JWT_COOKIE_NAME}, +}; + +pub async fn logout( + State(state): State, + jar: CookieJar, +) -> (CookieJar, Result) { + let cookie = match jar.get(JWT_COOKIE_NAME) { + Some(coockie) => coockie, + None => return (jar, Err(AuthAPIError::MissingToken)), + }; + + let token = cookie.value().to_owned(); + + if validate_token(&token, state.banned_tokens_store.clone()) + .await + .is_err() + { + return (jar, Err(AuthAPIError::InvalidToken)); + } + + if let Err(e) = state + .banned_tokens_store + .write() + .await + .add_token(token) + .await + { + return (jar, Err(AuthAPIError::UnexpectedError(e.into()))); + } + + let jar = jar.remove(cookie::Cookie::from(JWT_COOKIE_NAME)); + + (jar, Ok(StatusCode::OK)) +} diff --git a/auth-service/src/routes/mod.rs b/auth-service/src/routes/mod.rs new file mode 100644 index 000000000..848a1cf01 --- /dev/null +++ b/auth-service/src/routes/mod.rs @@ -0,0 +1,12 @@ +pub mod login; +pub mod logout; +pub mod signup; +pub mod verify_2fa; +pub mod verify_token; + +// re-export items from sub-modules +pub use login::*; +pub use logout::*; +pub use signup::*; +pub use verify_2fa::*; +pub use verify_token::*; diff --git a/auth-service/src/routes/signup.rs b/auth-service/src/routes/signup.rs new file mode 100644 index 000000000..0663bd8eb --- /dev/null +++ b/auth-service/src/routes/signup.rs @@ -0,0 +1,53 @@ +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use serde::{Deserialize, Serialize}; + +use crate::app_state::AppState; + +use crate::domain::User; + +use crate::domain::email::Email; +use crate::domain::password::HashedPassword; +use crate::AuthAPIError; + +#[derive(Deserialize)] +pub struct SignupRequest { + pub email: String, + pub password: String, + #[serde(rename = "requires2FA")] + pub requires_2fa: bool, +} + +// #[tracing::instrument(name = "Signup", skip_all, err(Debug))] +#[tracing::instrument(name = "Signup", skip_all)] +pub async fn signup( + State(state): State, + Json(request): Json, +) -> Result { + let email = Email::parse(request.email).map_err(|_| AuthAPIError::InvalidCredentials)?; + let password = HashedPassword::parse(request.password) + .await + .map_err(|_| AuthAPIError::InvalidCredentials)?; + // .map_err(|e| AuthAPIError::UnexpectedError(e.into()))?; + + let user = User::new(email.clone(), password, request.requires_2fa); + let mut user_store = state.user_store.write().await; + + if user_store.get_user(&email).await.is_ok() { + return Err(AuthAPIError::UserAlreadyExists); + } + + if let Err(e) = user_store.add_user(user).await { + return Err(AuthAPIError::UnexpectedError(e.into())); + } + + let response = Json(SignupResponse { + message: "User created successfully!".to_string(), + }); + + Ok((StatusCode::CREATED, response)) +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct SignupResponse { + pub message: String, +} diff --git a/auth-service/src/routes/verify_2fa.rs b/auth-service/src/routes/verify_2fa.rs new file mode 100644 index 000000000..4a345eae5 --- /dev/null +++ b/auth-service/src/routes/verify_2fa.rs @@ -0,0 +1,69 @@ +use crate::utils::auth::generate_auth_cookie; + +use crate::{ + app_state::AppState, + domain::{ + data_stores::{LoginAttemptId, TwoFACode}, + email::Email, + }, + AuthAPIError, +}; +use axum::{extract::State, response::IntoResponse, Json}; +use axum_extra::extract::CookieJar; +use serde::Deserialize; + +use color_eyre::eyre::{eyre, Context, Result}; + +pub async fn verify_2fa( + State(state): State, + jar: CookieJar, + Json(request): Json, +) -> (CookieJar, Result) { + let email = match Email::parse(request.email.clone()) { + Ok(email) => email, + Err(_) => return (jar, Err(AuthAPIError::InvalidCredentials)), + }; + + let login_attempt_id = match LoginAttemptId::parse(request.login_attempt_id.clone()) { + Ok(login_attempt_id) => login_attempt_id, + Err(_) => return (jar, Err(AuthAPIError::InvalidCredentials)), + }; + + let two_fa_code = match TwoFACode::parse(request.two_fa_code) { + Ok(two_fa_code) => two_fa_code, + Err(_) => return (jar, Err(AuthAPIError::InvalidCredentials)), + }; + + let mut two_fa_code_store = state.two_fa_code_store.write().await; + + let code_tuple = match two_fa_code_store.get_code(&email).await { + Ok(code_tuple) => code_tuple, + Err(_) => return (jar, Err(AuthAPIError::IncorrectCredentials)), + }; + + if !code_tuple.0.eq(&login_attempt_id) || !code_tuple.1.eq(&two_fa_code) { + return (jar, Err(AuthAPIError::IncorrectCredentials)); + } + + if let Err(e) = two_fa_code_store.remove_code(&email).await { + return (jar, Err(AuthAPIError::UnexpectedError(e.into()))); + } + + let cookie = match generate_auth_cookie(&email) { + Ok(cookie) => cookie, + Err(e) => return (jar, Err(AuthAPIError::UnexpectedError(e.into()))), + }; + + let updated_jar = jar.add(cookie); + + (updated_jar, Ok(())) +} + +#[derive(Deserialize, Debug)] +pub struct Verify2FARequest { + pub email: String, + #[serde(rename = "loginAttemptId")] + pub login_attempt_id: String, + #[serde(rename = "2FACode")] + pub two_fa_code: String, +} diff --git a/auth-service/src/routes/verify_token.rs b/auth-service/src/routes/verify_token.rs new file mode 100644 index 000000000..b098c1ff6 --- /dev/null +++ b/auth-service/src/routes/verify_token.rs @@ -0,0 +1,22 @@ +use crate::app_state::AppState; +use axum::Json; +use axum::{extract::State, http::StatusCode, response::IntoResponse}; +use serde::{Deserialize, Serialize}; + +use crate::domain::errors::AuthAPIError; +use crate::utils::auth::validate_token; + +#[derive(Deserialize, Serialize)] +pub struct VerifyTokenRequest { + pub token: String, +} + +pub async fn verify_token( + State(state): State, + Json(request): Json, +) -> Result { + match validate_token(&request.token, state.banned_tokens_store).await { + Ok(_) => Ok(StatusCode::OK), + Err(_) => Err(AuthAPIError::InvalidToken), + } +} diff --git a/auth-service/src/services/data_stores/banned_tokens_store.rs b/auth-service/src/services/data_stores/banned_tokens_store.rs new file mode 100644 index 000000000..964cf6d40 --- /dev/null +++ b/auth-service/src/services/data_stores/banned_tokens_store.rs @@ -0,0 +1,43 @@ +use std::collections::HashSet; + +use crate::domain::data_stores::{BannedTokenStore, BannedTokenStoreError}; + +#[derive(Default)] +pub struct HashsetBannedTokenStore { + pub banned_tokens: HashSet, +} + +#[async_trait::async_trait] +impl BannedTokenStore for HashsetBannedTokenStore { + async fn add_token(&mut self, token: String) -> Result<(), BannedTokenStoreError> { + self.banned_tokens.insert(token); + Ok(()) + } + + async fn contains_token(&self, token: &str) -> Result { + Ok(self.banned_tokens.contains(token)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_add_banned_token_should_succeed() { + let mut store = HashsetBannedTokenStore::default(); + + assert!(store.add_token(String::from("test_token")).await.is_ok()); + } + + #[tokio::test] + async fn test_exists_banned_token_should_succeed() { + let mut store = HashsetBannedTokenStore::default(); + + assert!(store.add_token(String::from("test_token")).await.is_ok()); + + assert!(store.contains_token("test_token").await.unwrap() == true); + + assert!(store.contains_token("invalid").await.unwrap() == false); + } +} diff --git a/auth-service/src/services/data_stores/hashmap_two_fa_code_store.rs b/auth-service/src/services/data_stores/hashmap_two_fa_code_store.rs new file mode 100644 index 000000000..1e9516b09 --- /dev/null +++ b/auth-service/src/services/data_stores/hashmap_two_fa_code_store.rs @@ -0,0 +1,107 @@ +use std::collections::HashMap; + +use crate::domain::{ + data_stores::{LoginAttemptId, TwoFACode, TwoFACodeStore, TwoFACodeStoreError}, + email::Email, +}; + +#[derive(Default)] +pub struct HashmapTwoFACodeStore { + codes: HashMap, +} + +// TODO: implement TwoFACodeStore for HashmapTwoFACodeStore +#[async_trait::async_trait] +impl TwoFACodeStore for HashmapTwoFACodeStore { + async fn add_code( + &mut self, + email: Email, + login_attempt_id: LoginAttemptId, + code: TwoFACode, + ) -> Result<(), TwoFACodeStoreError> { + self.codes.insert(email, (login_attempt_id, code)); + Ok(()) + } + + async fn remove_code(&mut self, email: &Email) -> Result<(), TwoFACodeStoreError> { + match self.codes.remove(email) { + Some(_) => Ok(()), + None => Err(TwoFACodeStoreError::LoginAttemptIdNotFound), + } + } + async fn get_code( + &self, + email: &Email, + ) -> Result<(LoginAttemptId, TwoFACode), TwoFACodeStoreError> { + match self.codes.get(email) { + Some(value) => Ok(value.clone()), + None => Err(TwoFACodeStoreError::LoginAttemptIdNotFound), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[tokio::test] + async fn test_add_code() { + let mut store = HashmapTwoFACodeStore::default(); + let email = Email::parse("test@example.com".to_string()).unwrap(); + let login_attempt_id = LoginAttemptId::default(); + let code = TwoFACode::default(); + + let result = store + .add_code(email.clone(), login_attempt_id.clone(), code.clone()) + .await; + + assert!(result.is_ok()); + assert_eq!(store.codes.get(&email), Some(&(login_attempt_id, code))); + } + + #[tokio::test] + async fn test_remove_code() { + let mut store = HashmapTwoFACodeStore::default(); + let email = Email::parse("test@example.com".to_string()).unwrap(); + let login_attempt_id = LoginAttemptId::default(); + let code = TwoFACode::default(); + + store + .codes + .insert(email.clone(), (login_attempt_id.clone(), code.clone())); + + let result = store.remove_code(&email).await; + + assert!(result.is_ok()); + assert_eq!(store.codes.get(&email), None); + } + + #[tokio::test] + async fn test_get_code() { + let mut store = HashmapTwoFACodeStore::default(); + let email = Email::parse("test@example.com".to_string()).unwrap(); + let login_attempt_id = LoginAttemptId::default(); + let code = TwoFACode::default(); + store + .codes + .insert(email.clone(), (login_attempt_id.clone(), code.clone())); + + let result = store.get_code(&email).await; + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), (login_attempt_id, code)); + } + + #[tokio::test] + async fn test_get_code_not_found() { + let store = HashmapTwoFACodeStore::default(); + let email = Email::parse("test@example.com".to_string()).unwrap(); + + let result = store.get_code(&email).await; + + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + TwoFACodeStoreError::LoginAttemptIdNotFound + ); + } +} diff --git a/auth-service/src/services/data_stores/hashmap_user_store.rs b/auth-service/src/services/data_stores/hashmap_user_store.rs new file mode 100644 index 000000000..1a6c48c49 --- /dev/null +++ b/auth-service/src/services/data_stores/hashmap_user_store.rs @@ -0,0 +1,104 @@ +use crate::domain::{ + data_stores::UserStore, data_stores::UserStoreError, email::Email, user::User, +}; +use std::collections::HashMap; + +#[derive(Default)] +pub struct HashmapUserStore { + pub users: HashMap, +} + +#[async_trait::async_trait] +impl UserStore for HashmapUserStore { + async fn add_user(&mut self, user: User) -> Result<(), UserStoreError> { + if self.users.contains_key(&user.email) { + return Err(UserStoreError::UserAlreadyExists); + } else { + self.users.insert(user.email.clone(), user); + return Ok(()); + } + } + + async fn get_user(&self, email: &Email) -> Result { + if let Some(user) = self.users.get(email) { + Ok(user.clone()) + } else { + Err(UserStoreError::UserNotFound) + } + } + + async fn validate_user(&self, email: &Email, raw_password: &str) -> Result<(), UserStoreError> { + // let user = self.get_user(email).await?; + let user: &User = self.users.get(email).ok_or(UserStoreError::UserNotFound)?; + + user.password // updated password verification + .verify_raw_password(raw_password) + .await + .map_err(|_| UserStoreError::InvalidCredentials) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::password::HashedPassword; + + fn helper_build_user_email(email: &str) -> Email { + Email::parse(email.into()).expect("expects a valid@example.email ") + } + + async fn helper_build_user_password(pass: &str) -> HashedPassword { + HashedPassword::parse(pass.into()) + .await + .expect("expects a valid password minimum 8 chars long") + } + + #[tokio::test] + async fn test_add_user() { + let mut store = HashmapUserStore::default(); + let user = store + .add_user(User { + email: helper_build_user_email("test@example.com"), + password: helper_build_user_password("12345678").await, + requires_2fa: false, + }) + .await; + assert!(user.is_ok()); + } + + #[tokio::test] + async fn test_get_user() { + let mut store = HashmapUserStore::default(); + let user = User { + email: helper_build_user_email("test@example.com"), + password: helper_build_user_password("password").await, + requires_2fa: false, + }; + store.add_user(user).await.unwrap(); + + let retrieved_user = store + .get_user(&helper_build_user_email("test@example.com")) + .await + .unwrap(); + assert_eq!( + retrieved_user.email, + helper_build_user_email("test@example.com") + ); + } + + #[tokio::test] + async fn test_validate_user() { + let mut store = HashmapUserStore::default(); + let email = helper_build_user_email("test@example.com"); + + let user = User { + email: email.clone(), + password: helper_build_user_password("password").await, + requires_2fa: false, + }; + store.add_user(user).await.unwrap(); + + let is_user_valid_result = store.validate_user(&email, "password").await; + assert!(is_user_valid_result.is_ok()); + } +} diff --git a/auth-service/src/services/data_stores/mod.rs b/auth-service/src/services/data_stores/mod.rs new file mode 100644 index 000000000..cb095dfe4 --- /dev/null +++ b/auth-service/src/services/data_stores/mod.rs @@ -0,0 +1,6 @@ +pub mod banned_tokens_store; +pub mod hashmap_two_fa_code_store; +pub mod hashmap_user_store; +pub mod postgres_user_store; +pub mod redis_banned_token_store; +pub mod redis_two_fa_code_store; diff --git a/auth-service/src/services/data_stores/postgres_user_store.rs b/auth-service/src/services/data_stores/postgres_user_store.rs new file mode 100644 index 000000000..216dd29a5 --- /dev/null +++ b/auth-service/src/services/data_stores/postgres_user_store.rs @@ -0,0 +1,77 @@ +use sqlx::PgPool; + +use crate::domain::{ + data_stores::{UserStore, UserStoreError}, + email::Email, + password::HashedPassword, + User, +}; + +use color_eyre::eyre::{eyre, Result}; + +pub struct PostgresUserStore { + pool: PgPool, +} + +impl PostgresUserStore { + pub fn new(pool: PgPool) -> Self { + Self { pool } + } +} + +#[async_trait::async_trait] +impl UserStore for PostgresUserStore { + #[tracing::instrument(name = "Adding user to PostgreSQL", skip_all)] + async fn add_user(&mut self, user: User) -> Result<(), UserStoreError> { + sqlx::query!( + "INSERT INTO users (email, password_hash, requires_2fa) VALUES ($1, $2, $3)", + user.email.as_ref(), + user.password.as_ref(), + user.requires_2fa, + ) + .execute(&self.pool) + .await + .map_err(|e| UserStoreError::UnexpectedError(e.into())) + .ok(); + + Ok(()) + } + + #[tracing::instrument(name = "Getting user from PostgreSQL", skip_all)] + async fn get_user(&self, email: &Email) -> Result { + #[derive(Debug, Clone, sqlx::FromRow, Default)] + struct UserSql { + email: String, + password_hash: String, + requires_2fa: bool, + } + + let user = sqlx::query_as!( + UserSql, + "SELECT email, password_hash, requires_2fa FROM users WHERE email = $1", + email.as_ref(), + ) + .fetch_optional(&self.pool) + .await + .map_err(|_| UserStoreError::UserNotFound)? + .ok_or(UserStoreError::UserNotFound)?; + + Ok(User::new( + // Email::parse(user.email).expect("Valid email"), + Email::parse(user.email).map_err(|e| UserStoreError::UnexpectedError(eyre!(e)))?, + HashedPassword::parse_password_hash(user.password_hash) + .map_err(|e| UserStoreError::UnexpectedError(eyre!(e)))?, + user.requires_2fa, + )) + } + + #[tracing::instrument(name = "Validating user credentials in PostgreSQL", skip_all)] + async fn validate_user(&self, email: &Email, raw_password: &str) -> Result<(), UserStoreError> { + let user: User = self.get_user(email).await?; + + user.password // updated password verification + .verify_raw_password(raw_password) + .await + .map_err(|_| UserStoreError::InvalidCredentials) + } +} diff --git a/auth-service/src/services/data_stores/redis_banned_token_store.rs b/auth-service/src/services/data_stores/redis_banned_token_store.rs new file mode 100644 index 000000000..a924781b6 --- /dev/null +++ b/auth-service/src/services/data_stores/redis_banned_token_store.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use redis::{Commands, Connection}; +use tokio::sync::RwLock; + +use crate::{ + domain::data_stores::{BannedTokenStore, BannedTokenStoreError}, + utils::auth::TOKEN_TTL_SECONDS, +}; + +use color_eyre::eyre::{eyre, Result}; + +pub struct RedisBannedTokenStore { + conn: Arc>, +} + +impl RedisBannedTokenStore { + pub fn new(conn: Arc>) -> Self { + Self { conn } + } +} + +#[async_trait::async_trait] +impl BannedTokenStore for RedisBannedTokenStore { + async fn add_token(&mut self, token: String) -> Result<(), BannedTokenStoreError> { + let key: String = get_key(token.as_str()); + let mut connection = self.conn.write().await; + + connection + .set_ex(&key, true, TOKEN_TTL_SECONDS as u64) + .map_err(|e| BannedTokenStoreError::UnexpectedError(e.into())) + } + + async fn contains_token(&self, token: &str) -> Result { + // Check if the token exists by calling the exists method on the Redis connection + //todo!() + let key: String = get_key(token); + let mut connection = self.conn.write().await; + let exists = connection + .exists(&key) + .map_err(|e| BannedTokenStoreError::UnexpectedError(e.into()))?; + Ok(exists) + } +} + +// We are using a key prefix to prevent collisions and organize data! +const BANNED_TOKEN_KEY_PREFIX: &str = "banned_token:"; + +fn get_key(token: &str) -> String { + format!("{}{}", BANNED_TOKEN_KEY_PREFIX, token) +} diff --git a/auth-service/src/services/data_stores/redis_two_fa_code_store.rs b/auth-service/src/services/data_stores/redis_two_fa_code_store.rs new file mode 100644 index 000000000..b8d6b3170 --- /dev/null +++ b/auth-service/src/services/data_stores/redis_two_fa_code_store.rs @@ -0,0 +1,126 @@ +use std::sync::Arc; + +use redis::{Commands, Connection}; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use crate::domain::{ + data_stores::{LoginAttemptId, TwoFACode, TwoFACodeStore, TwoFACodeStoreError}, + email::Email, +}; + +pub struct RedisTwoFACodeStore { + conn: Arc>, +} + +impl RedisTwoFACodeStore { + pub fn new(conn: std::sync::Arc>) -> Self { + Self { conn } + } +} + +#[async_trait::async_trait] +impl TwoFACodeStore for RedisTwoFACodeStore { + async fn add_code( + &mut self, + email: Email, + login_attempt_id: LoginAttemptId, + code: TwoFACode, + ) -> Result<(), TwoFACodeStoreError> { + // TODO: + // 1. Create a new key using the get_key helper function. + // 2. Create a TwoFATuple instance. + // 3. Use serde_json::to_string to serialize the TwoFATuple instance into a JSON string. + // Return TwoFACodeStoreError::UnexpectedError if serialization fails. + // 4. Call the set_ex command on the Redis connection to set a new key/value pair with an expiration time (TTL). + // The value should be the serialized 2FA tuple. + // The expiration time should be set to TEN_MINUTES_IN_SECONDS. + // Return TwoFACodeStoreError::UnexpectedError if casting fails or the call to set_ex fails. + + // todo!() + let key = get_key(&email); + + let two_fa_tuple = TwoFATuple( + login_attempt_id.as_ref().to_owned(), + code.as_ref().to_owned(), + ); + let serialized = serde_json::to_string(&two_fa_tuple) + .map_err(|e| TwoFACodeStoreError::UnexpectedError(e.into()))?; + + let res: () = self + .conn + .write() + .await + .set_ex(&key, &serialized, TEN_MINUTES_IN_SECONDS) + .map_err(|e| TwoFACodeStoreError::UnexpectedError(e.into()))?; + + /* + hint: in edition 2024, the requirement `!: FromRedisValue` will fail + hint: use `()` annotations to avoid fallback changes: `::<_, _, ()>` + */ + + Ok(res) + } + + async fn remove_code(&mut self, email: &Email) -> Result<(), TwoFACodeStoreError> { + // TODO: + // 1. Create a new key using the get_key helper function. + // 2. Call the del command on the Redis connection to delete the 2FA code entry. + // Return TwoFACodeStoreError::UnexpectedError if the operation fails. + + // todo!() + + let key = get_key(&email); + let res: () = self + .conn + .write() + .await + .del(&key) + .map_err(|e| TwoFACodeStoreError::UnexpectedError(e.into()))?; + + Ok(res) + } + + async fn get_code( + &self, + email: &Email, + ) -> Result<(LoginAttemptId, TwoFACode), TwoFACodeStoreError> { + // TODO: + //+ 1. Create a new key using the get_key helper function. + // 2. Call the get command on the Redis connection to get the value stored for the key. + // Return TwoFACodeStoreError::LoginAttemptIdNotFound if the operation fails. + // If the operation succeeds, call serde_json::from_str to parse the JSON string into a TwoFATuple. + // Then, parse the login attempt ID string and 2FA code string into a LoginAttemptId and TwoFACode type respectively. + // Return TwoFACodeStoreError::UnexpectedError if parsing fails. + + //todo!() + let key = get_key(&email); + + let mut connection = self.conn.write().await; + let res: Option = connection + .get(key) + .map_err(|_| TwoFACodeStoreError::LoginAttemptIdNotFound)?; + + let res: String = res.ok_or(TwoFACodeStoreError::LoginAttemptIdNotFound)?; + let (login_attempt_id, code) = serde_json::from_str(res.as_str()) + .map_err(|e| TwoFACodeStoreError::UnexpectedError(e.into()))?; + + let login_attempt_id = LoginAttemptId::parse(login_attempt_id) + .map_err(|e| TwoFACodeStoreError::UnexpectedError(e.into()))?; + + let code = + TwoFACode::parse(code).map_err(|e| TwoFACodeStoreError::UnexpectedError(e.into()))?; + + Ok((login_attempt_id, code)) + } +} + +#[derive(Serialize, Deserialize)] +struct TwoFATuple(pub String, pub String); + +const TEN_MINUTES_IN_SECONDS: u64 = 600; +const TWO_FA_CODE_PREFIX: &str = "two_fa_code:"; + +fn get_key(email: &Email) -> String { + format!("{}{}", TWO_FA_CODE_PREFIX, email.as_ref()) +} diff --git a/auth-service/src/services/mock_email_client.rs b/auth-service/src/services/mock_email_client.rs new file mode 100644 index 000000000..3031d00c3 --- /dev/null +++ b/auth-service/src/services/mock_email_client.rs @@ -0,0 +1,24 @@ +use crate::domain::{email::Email, email_client::EmailClient, email_client::EmailError}; + +#[derive(Default)] +pub struct MockEmailClient; + +#[async_trait::async_trait] +impl EmailClient for MockEmailClient { + async fn send_email( + &self, + recipient: &Email, + subject: &str, + content: &str, + ) -> Result<(), EmailError> { + // Our mock email client will simply log the recipient, subject, and content to standard output + println!( + "Sending email to {} with subject: {} and content: {}", + recipient.as_ref(), + subject, + content + ); + + Ok(()) + } +} diff --git a/auth-service/src/services/mod.rs b/auth-service/src/services/mod.rs new file mode 100644 index 000000000..0bd85353c --- /dev/null +++ b/auth-service/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod data_stores; +pub mod mock_email_client; diff --git a/auth-service/src/utils/auth.rs b/auth-service/src/utils/auth.rs new file mode 100644 index 000000000..6f381f0a4 --- /dev/null +++ b/auth-service/src/utils/auth.rs @@ -0,0 +1,170 @@ +use axum_extra::extract::cookie::{Cookie, SameSite}; +use chrono::Utc; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Validation}; +use serde::{Deserialize, Serialize}; + +use super::constants::{JWT_COOKIE_NAME, JWT_SECRET}; +use crate::domain::email::Email; +use jsonwebtoken::errors::{Error, ErrorKind}; + +// Create cookie with a new JWT auth token +pub fn generate_auth_cookie(email: &Email) -> Result, GenerateTokenError> { + let token = generate_auth_token(email)?; + Ok(create_auth_cookie(token)) +} + +// Create cookie and set the value to the passed-in token string +fn create_auth_cookie(token: String) -> Cookie<'static> { + let cookie = Cookie::build((JWT_COOKIE_NAME, token)) + .path("/") // apply cookie to all URLs on the server + .http_only(true) // prevent JavaScript from accessing the cookie + .same_site(SameSite::Lax) // send cookie with "same-site" requests, and with "cross-site" top-level navigations. + .build(); + + cookie +} +use color_eyre::eyre::Result; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum GenerateTokenError { + #[error("Token error")] + TokenError(#[source] Error), + #[error("Unexpected error")] + UnexpectedError, +} + +// This value determines how long the JWT auth token is valid for +pub const TOKEN_TTL_SECONDS: i64 = 600; // 10 minutes + +// Create JWT auth token +fn generate_auth_token(email: &Email) -> Result { + let delta = chrono::Duration::try_seconds(TOKEN_TTL_SECONDS) + .ok_or(GenerateTokenError::UnexpectedError)?; + + // Create JWT expiration time + let exp = Utc::now() + .checked_add_signed(delta) + .ok_or(GenerateTokenError::UnexpectedError)? + .timestamp(); + + // Cast exp to a usize, which is what Claims expects + let exp: usize = exp + .try_into() + .map_err(|_| GenerateTokenError::UnexpectedError)?; + + let sub = email.as_ref().to_owned(); + + let claims = Claims { sub, exp }; + + create_token(&claims).map_err(GenerateTokenError::TokenError) +} + +// Check if JWT auth token is valid by decoding it using the JWT secret +pub async fn validate_token( + token: &str, + banned_tokens_store: crate::app_state::BannedTokenStoreType, +) -> Result { + let banned_token = banned_tokens_store.read().await.contains_token(token).await; + + // println!("\x1b[32m contains banned_token {:?} \x1b[0m", banned_token); + + match banned_token { + Ok(value) => { + if value { + return Err(Error::from(ErrorKind::InvalidToken)); + } + } + Err(_) => { + return Err(Error::from(ErrorKind::InvalidToken)); + } + } + + decode::( + token, + &DecodingKey::from_secret(JWT_SECRET.as_bytes()), + &Validation::default(), + ) + .map(|data| data.claims) +} + +// Create JWT auth token by encoding claims using the JWT secret +fn create_token(claims: &Claims) -> Result { + encode( + &jsonwebtoken::Header::default(), + &claims, + &EncodingKey::from_secret(JWT_SECRET.as_bytes()), + ) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: usize, +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[tokio::test] + async fn test_generate_auth_cookie() { + let email = Email::parse("test@example.com".to_owned()).unwrap(); + let cookie = generate_auth_cookie(&email).unwrap(); + assert_eq!(cookie.name(), JWT_COOKIE_NAME); + assert_eq!(cookie.value().split('.').count(), 3); + assert_eq!(cookie.path(), Some("/")); + assert_eq!(cookie.http_only(), Some(true)); + assert_eq!(cookie.same_site(), Some(SameSite::Lax)); + } + + #[tokio::test] + async fn test_create_auth_cookie() { + let token = "test_token".to_owned(); + let cookie = create_auth_cookie(token.clone()); + assert_eq!(cookie.name(), JWT_COOKIE_NAME); + assert_eq!(cookie.value(), token); + assert_eq!(cookie.path(), Some("/")); + assert_eq!(cookie.http_only(), Some(true)); + assert_eq!(cookie.same_site(), Some(SameSite::Lax)); + } + + #[tokio::test] + async fn test_generate_auth_token() { + let email = Email::parse("test@example.com".to_owned()).unwrap(); + let result = generate_auth_token(&email).unwrap(); + assert_eq!(result.split('.').count(), 3); + } + + #[tokio::test] + async fn test_validate_token_with_valid_token() { + let email = Email::parse("test@example.com".to_owned()).unwrap(); + let token = generate_auth_token(&email).unwrap(); + let banned_token_store = std::sync::Arc::new(tokio::sync::RwLock::new( + crate::services::data_stores::banned_tokens_store::HashsetBannedTokenStore::default(), + )); + let result = validate_token(&token, banned_token_store) + .await + .expect("Claims and not banned_token is Store"); + + assert_eq!(result.sub, "test@example.com"); + + let exp = Utc::now() + .checked_add_signed(chrono::Duration::try_minutes(9).expect("valid duration")) + .expect("valid timestamp") + .timestamp(); + + assert!(result.exp > exp as usize); + } + + #[tokio::test] + async fn test_validate_token_with_invalid_token() { + let token = "invalid_token".to_owned(); + let banned_token_store = std::sync::Arc::new(tokio::sync::RwLock::new( + crate::services::data_stores::banned_tokens_store::HashsetBannedTokenStore::default(), + )); + let result = validate_token(&token, banned_token_store).await; + assert!(result.is_err()); + } +} diff --git a/auth-service/src/utils/constants.rs b/auth-service/src/utils/constants.rs new file mode 100644 index 000000000..2d26210c5 --- /dev/null +++ b/auth-service/src/utils/constants.rs @@ -0,0 +1,46 @@ +use dotenvy::dotenv; +use lazy_static::lazy_static; +use std::env as std_env; + +lazy_static! { + pub static ref JWT_SECRET: String = set_token(); + pub static ref DATABASE_URL: String = set_db_url(); + pub static ref REDIS_HOST_NAME: String = set_redis_host(); // New! +} + +fn set_token() -> String { + dotenv().ok(); + let secret = std_env::var(env::JWT_SECRET_ENV_VAR).expect("JWT_SECRET must be set."); + if secret.is_empty() { + panic!("JWT_SECRET must not be empty."); + } + secret +} + +fn set_db_url() -> String { + dotenv().ok(); + std_env::var(env::DATABASE_URL_ENV_VAR).expect("DATABASE_URL must be set.") +} + +// New! +fn set_redis_host() -> String { + dotenv().ok(); + std_env::var(env::REDIS_HOST_NAME_ENV_VAR).unwrap_or(DEFAULT_REDIS_HOSTNAME.to_owned()) +} + +pub mod env { + pub const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL"; + pub const JWT_SECRET_ENV_VAR: &str = "JWT_SECRET"; + pub const REDIS_HOST_NAME_ENV_VAR: &str = "REDIS_HOST_NAME"; // New! +} + +pub const JWT_COOKIE_NAME: &str = "jwt"; +pub const DEFAULT_REDIS_HOSTNAME: &str = "127.0.0.1"; // New! + +pub mod prod { + pub const APP_ADDRESS: &str = "0.0.0.0:3000"; +} + +pub mod test { + pub const APP_ADDRESS: &str = "127.0.0.1:0"; +} diff --git a/auth-service/src/utils/mod.rs b/auth-service/src/utils/mod.rs new file mode 100644 index 000000000..b008d1bf3 --- /dev/null +++ b/auth-service/src/utils/mod.rs @@ -0,0 +1,3 @@ +pub mod auth; +pub mod constants; +pub mod tracing; diff --git a/auth-service/src/utils/tracing.rs b/auth-service/src/utils/tracing.rs new file mode 100644 index 000000000..ca5469d03 --- /dev/null +++ b/auth-service/src/utils/tracing.rs @@ -0,0 +1,75 @@ +use std::time::Duration; + +use axum::{body::Body, extract::Request, response::Response}; +use tracing::{Level, Span}; + +use color_eyre::eyre::Result; +use tracing_error::ErrorLayer; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{fmt, EnvFilter}; + +pub fn init_tracing() -> Result<()> { + // Create a formatting layer for tracing output with a compact format + let fmt_layer = fmt::layer().compact(); + + // Create a filter layer to control the verbosity of logs + // Try to get the filter configuration from the environment variables + // If it fails, default to the "info" log level + let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?; + + // Build the tracing subscriber registry with the formatting layer, + // the filter layer, and the error layer for enhanced error reporting + tracing_subscriber::registry() + .with(filter_layer) // Add the filter layer to control log verbosity + .with(fmt_layer) // Add the formatting layer for compact log output + .with(ErrorLayer::default()) // Add the error layer to capture error contexts + .init(); // Initialize the tracing subscriber + + Ok(()) +} + +// Creates a new tracing span with a unique request ID for each incoming request. +// This helps in tracking and correlating logs for individual requests. +pub fn make_span_with_request_id(request: &Request) -> Span { + let request_id = uuid::Uuid::new_v4(); + tracing::span!( + Level::INFO, + "[REQUEST]", + method = tracing::field::display(request.method()), + uri = tracing::field::display(request.uri()), + version = tracing::field::debug(request.version()), + request_id = tracing::field::display(request_id), + ) +} + +// Logs an event indicating the start of a request. +pub fn on_request(_request: &Request, _span: &Span) { + tracing::event!(Level::INFO, "[REQUEST START]"); +} + +// Logs an event indicating the end of a request, including its latency and status code. +// If the status code indicates an error (4xx or 5xx), it logs at the ERROR level. +pub fn on_response(response: &Response, latency: Duration, _span: &Span) { + let status = response.status(); + let status_code = status.as_u16(); + let status_code_class = status_code / 100; + + match status_code_class { + 4..=5 => { + tracing::event!( + Level::ERROR, + latency = ?latency, + status = status_code, + "[REQUEST END]" + ) + } + _ => { + tracing::event!( + Level::INFO, + latency = ?latency, + status = status_code, + "[REQUEST END]" + ) + } + }; +} diff --git a/auth-service/test_macros/Cargo.lock b/auth-service/test_macros/Cargo.lock new file mode 100644 index 000000000..7dc1e2510 --- /dev/null +++ b/auth-service/test_macros/Cargo.lock @@ -0,0 +1,47 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "test_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/auth-service/test_macros/Cargo.toml b/auth-service/test_macros/Cargo.toml new file mode 100644 index 000000000..2131feadd --- /dev/null +++ b/auth-service/test_macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "test_macros" +version = "0.1.0" +edition = "2024" + + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" diff --git a/auth-service/test_macros/src/lib.rs b/auth-service/test_macros/src/lib.rs new file mode 100644 index 000000000..9d8488a0e --- /dev/null +++ b/auth-service/test_macros/src/lib.rs @@ -0,0 +1,16 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{ItemFn, parse_macro_input}; + +#[proc_macro_attribute] +pub fn auto_db_cleanup(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut input = parse_macro_input!(item as ItemFn); + + input.block.stmts.push(syn::parse_quote! { + app.clean_up().await; + }); + + TokenStream::from(quote! { + #input + }) +} diff --git a/auth-service/tests/api/helpers.rs b/auth-service/tests/api/helpers.rs new file mode 100644 index 000000000..8f5791791 --- /dev/null +++ b/auth-service/tests/api/helpers.rs @@ -0,0 +1,262 @@ +use auth_service::{utils, Application}; + +use auth_service::app_state::AppState; +// use auth_service::services::data_stores::hashmap_two_fa_code_store::HashmapTwoFACodeStore; +//use auth_service::services::data_stores::hashmap_user_store::HashmapUserStore; +// use auth_service::services::data_stores::banned_tokens_store::HashsetBannedTokenStore; +use auth_service::services::data_stores::postgres_user_store::PostgresUserStore; +use auth_service::services::data_stores::redis_banned_token_store::RedisBannedTokenStore; +use auth_service::services::data_stores::redis_two_fa_code_store::RedisTwoFACodeStore; + +use auth_service::services::mock_email_client::MockEmailClient; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub struct TestApp { + pub address: String, + pub cookie_jar: Arc, + pub http_client: reqwest::Client, + pub banned_tokens_store: Arc>, + pub two_fa_code_store: Arc>, + pub database_name: String, + pub clean_up_called: bool, +} + +impl TestApp { + pub async fn new() -> Self { + //let user_store = Arc::new(RwLock::new(HashmapUserStore::default())); + let (database_name, pg_pool) = configure_postgresql().await; + let user_store = Arc::new(RwLock::new(PostgresUserStore::new(pg_pool))); + + let redis_conn = std::sync::Arc::new(tokio::sync::RwLock::new(configure_redis())); + let banned_tokens_store = std::sync::Arc::new(tokio::sync::RwLock::new( + RedisBannedTokenStore::new(redis_conn.clone()), + )); + let two_fa_code_store = std::sync::Arc::new(tokio::sync::RwLock::new( + RedisTwoFACodeStore::new(redis_conn), + )); + + let email_client = + std::sync::Arc::new(tokio::sync::RwLock::new(MockEmailClient::default())); + let app_state = AppState::new( + user_store, + banned_tokens_store.clone(), + two_fa_code_store.clone(), + email_client, + ); + + let app = Application::build(app_state, utils::constants::test::APP_ADDRESS) + .await + .expect("Failed to build app"); + + let address = format!("http://{}", app.address.clone()); + + #[allow(clippy::let_underscore_future)] + let _ = tokio::spawn(app.run()); + + let cookie_jar = Arc::new(reqwest::cookie::Jar::default()); + let http_client = reqwest::Client::builder() + .cookie_provider(cookie_jar.clone()) + .build() + .unwrap(); + + Self { + address, + cookie_jar, + http_client, + banned_tokens_store, + two_fa_code_store, + database_name, + clean_up_called: false, + } + } + + pub async fn get_root(&self) -> reqwest::Response { + self.http_client + .get(&format!("{}/", &self.address)) + .send() + .await + .expect("Failed to execute request.") + } + + pub async fn post_signup(&self, body: &Body) -> reqwest::Response + where + Body: serde::Serialize, + { + self.http_client + .post(&format!("{}/signup", &self.address)) + .json(body) + .send() + .await + .expect("Failed to execute request.") + } + + pub async fn post_login(&self, body: &Body) -> reqwest::Response + where + Body: serde::Serialize, + { + self.http_client + .post(&format!("{}/login", &self.address)) + .json(body) + .send() + .await + .expect("Failed to execute request.") + } + + pub async fn post_logout(&self) -> reqwest::Response { + let json = "{}"; + self.http_client + .post(&format!("{}/logout", &self.address)) + .json(json) + .send() + .await + .expect("Failed to execute request.") + } + pub async fn post_verify2fa(&self, body: &Body) -> reqwest::Response + where + Body: serde::Serialize, + { + self.http_client + .post(&format!("{}/verify-2fa", &self.address)) + .json(body) + .send() + .await + .expect("Failed to execute request.") + } + + pub async fn post_verify_token(&self, body: &Body) -> reqwest::Response + where + Body: serde::Serialize, + { + self.http_client + .post(format!("{}/verify-token", &self.address)) + .json(body) + .send() + .await + .expect("Failed to execute request.") + } + + pub async fn clean_up(&mut self) { + // if !self.clean_up_called.load(Ordering::Relaxed) { + if !self.clean_up_called { + let db_name = self.database_name.clone(); + // let clean_up_called = self.clean_up_called.clone(); + + delete_database(&db_name).await; + // tokio::spawn(async move { + // clean_up_called.store(true, Ordering::Relaxed); + // }); + self.clean_up_called = true; + } + } +} + +impl Drop for TestApp { + fn drop(&mut self) { + if !self.clean_up_called { + panic!( + "TestApp clean_up() was not called before drop() for database {}", + self.database_name + ); + } + } +} + +pub fn get_random_email() -> String { + format!("{}@example.com", uuid::Uuid::new_v4()) +} + +fn configure_redis() -> redis::Connection { + auth_service::get_redis_client(utils::constants::REDIS_HOST_NAME.to_owned()) + .expect("Failed to get Redis client") + .get_connection() + .expect("Failed to get Redis connection") +} + +async fn configure_postgresql() -> (String, sqlx::PgPool) { + let postgresql_conn_url = auth_service::utils::constants::DATABASE_URL.to_owned(); + + // We are creating a new database for each test case, and we need to ensure each database has a unique name! + let db_name = uuid::Uuid::new_v4().to_string(); + + configure_database(&postgresql_conn_url, &db_name).await; + + let postgresql_conn_url_with_db = format!("{}/{}", postgresql_conn_url, db_name); + + // Create a new connection pool and return it + let pg_pool = auth_service::get_postgres_pool(&postgresql_conn_url_with_db) + .await + .expect("Failed to create Postgres connection pool!"); + + (db_name, pg_pool) +} + +async fn configure_database(db_conn_string: &str, db_name: &str) { + use sqlx::postgres::PgPoolOptions; + use sqlx::Executor; + + // Create database connection + let connection = PgPoolOptions::new() + .connect(db_conn_string) + .await + .expect("Failed to create Postgres connection pool."); + + // Create a new database + connection + .execute(format!(r#"CREATE DATABASE "{}";"#, db_name).as_str()) + .await + .expect("Failed to create database."); + + // Connect to new database + let db_conn_string = format!("{}/{}", db_conn_string, db_name); + + let connection = PgPoolOptions::new() + .connect(&db_conn_string) + .await + .expect("Failed to create Postgres connection pool."); + + // Run migrations against new database + sqlx::migrate!() + .run(&connection) + .await + .expect("Failed to migrate the database"); +} + +async fn delete_database(db_name: &str) { + use sqlx::postgres::PgConnectOptions; + use sqlx::Executor; + use sqlx::{Connection, PgConnection}; + use std::str::FromStr; + + let postgresql_conn_url: String = auth_service::utils::constants::DATABASE_URL.to_owned(); + + let connection_options = PgConnectOptions::from_str(&postgresql_conn_url) + .expect("Failed to parse PostgreSQL connection string"); + + let mut connection = PgConnection::connect_with(&connection_options) + .await + .expect("Failed to connect to Postgres"); + + // Kill any active connections to the database + connection + .execute( + format!( + r#" + SELECT pg_terminate_backend(pg_stat_activity.pid) + FROM pg_stat_activity + WHERE pg_stat_activity.datname = '{}' + AND pid <> pg_backend_pid(); + "#, + db_name + ) + .as_str(), + ) + .await + .expect("Failed to drop the database."); + + // Drop the database + connection + .execute(format!(r#"DROP DATABASE "{}";"#, db_name).as_str()) + .await + .expect("Failed to drop the database."); +} diff --git a/auth-service/tests/api/login.rs b/auth-service/tests/api/login.rs new file mode 100644 index 000000000..b943e1bec --- /dev/null +++ b/auth-service/tests/api/login.rs @@ -0,0 +1,201 @@ +use crate::helpers::TestApp; +use test_macros::auto_db_cleanup; + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_206_if_valid_credentials_and_2fa_enabled() { + let mut app = TestApp::new().await; + let email = "example@email.test"; + let password = "12345678"; + + { + // create new test User + let response = app + .post_signup(&serde_json::json!({ + "email": email, + "password": password, + "requires2FA": true + })) + .await; + assert_eq!(response.status().as_u16(), 201); + } + + let response = app + .post_login(&serde_json::json!({ + "email": email, + "password": password + })) + .await; + + assert_eq!(response.status().as_u16(), 206); + + assert_eq!( + response + .json::() + .await + .expect("Could not deserialize response body to TwoFactorAuthResponse") + .message, + "2FA required".to_owned() + ); + //todo!(); +} +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_422_if_malformed_credentials() { + let email = "example@email.test"; + + let password = "12345678"; + + let test_cases = &[ + serde_json::json!({ + "password": password, + }), + serde_json::json!({ + "email":email, + }), + serde_json::json!({ + "password": 1111, + "email":email + }), + serde_json::json!({ + "password": password, + "email":true, + }), + serde_json::json!({ + "password": true, + "email":true, + }), + ]; + + let mut app = TestApp::new().await; + helper_post_login_test_cases(test_cases, 422, &mut app).await; +} +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_400_if_invalid_input() { + let email = "example@email.test"; + let email_invalid = "example_mail.test"; + + let password = "12345678"; + let password_invalid = "1234567"; + + let test_cases = &[ + serde_json::json!({ + "email":"", + "password": "", + }), + serde_json::json!({ + "email":"", + "password": password, + }), + serde_json::json!({ + "email":email, + "password": "", + }), + serde_json::json!({ + "email":email, + "password": password_invalid, + }), + serde_json::json!({ + "email":email_invalid, + "password": password, + }), + serde_json::json!({ + "email":email_invalid, + "password": password_invalid, + }), + ]; + + let mut app = TestApp::new().await; + + helper_post_login_test_cases(test_cases, 400, &mut app).await; +} +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_401_if_incorrect_credentials() { + let email = "example@email.test"; + let email_invalid = "_example@email.test"; + + let password = "12345678"; + let password_invalid = "_12345678"; + + //{ + // create new test User + let mut app = TestApp::new().await; + let response = app + .post_signup(&serde_json::json!({ + "email": email, + "password": password, + "requires2FA": false + })) + .await; + assert_eq!(response.status().as_u16(), 201); + //} + + let test_cases = &[ + serde_json::json!({ + "email":email, + "password": password_invalid, + }), + serde_json::json!({ + "email":email_invalid, + "password": password, + }), + ]; + + helper_post_login_test_cases(test_cases, 401, &mut app).await; +} +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_200_if_valid_credentials_and_2fa_disabled() { + let email = "example@email.test"; + let password = "12345678"; + + let mut app = TestApp::new().await; + { + // create new test User + let response = app + .post_signup(&serde_json::json!({ + "email": email, + "password": password, + "requires2FA": false + })) + .await; + assert_eq!(response.status().as_u16(), 201); + } + + let login_body = serde_json::json!({ + "email": email, + "password": password, + }); + + let response = app.post_login(&login_body).await; + + assert_eq!(response.status().as_u16(), 200); + + let auth_cookie = response + .cookies() + .find(|cookie| cookie.name() == auth_service::utils::constants::JWT_COOKIE_NAME) + .expect("No auth cookie found"); + + assert!(!auth_cookie.value().is_empty()); +} + +/// Test Helper method +async fn helper_post_login_test_cases( + test_cases: &[serde_json::Value], + expected_status_code: u16, + app: &mut TestApp, +) { + // let app = TestApp::new().await; + for test_case in test_cases.iter() { + let response = app.post_login(test_case).await; + + assert_eq!( + response.status().as_u16(), + expected_status_code, + "Failed for input: {:?}", + test_case + ); + } +} diff --git a/auth-service/tests/api/logout.rs b/auth-service/tests/api/logout.rs new file mode 100644 index 000000000..61d506f23 --- /dev/null +++ b/auth-service/tests/api/logout.rs @@ -0,0 +1,83 @@ +use auth_service::{domain::data_stores::BannedTokenStore, utils::constants::JWT_COOKIE_NAME}; +use reqwest::Url; + +use crate::helpers::TestApp; +use test_macros::auto_db_cleanup; + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_400_if_jwt_cookie_missing() { + let mut app = TestApp::new().await; + let logout_response = app.post_logout().await; + + assert_eq!(logout_response.status().as_u16(), 400); +} +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_401_if_invalid_token() { + let mut app = TestApp::new().await; + + // add invalid cookie + app.cookie_jar.add_cookie_str( + &format!( + "{}=invalid; HttpOnly; SameSite=Lax; Secure; Path=/", + JWT_COOKIE_NAME + ), + &Url::parse("http://127.0.0.1").expect("Failed to parse URL"), + ); + + let logout_response = app.post_logout().await; + + assert_eq!(logout_response.status().as_u16(), 401); +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_200_if_jwt_cookie_is_valid() { + let mut app = TestApp::new().await; + + let email = "example@email.test"; + let password = "12345678"; + + assert_eq!( + app.post_signup(&serde_json::json!({ + "email": email, + "password": password, + "requires2FA": false + })) + .await + .status() + .as_u16(), + 201 + ); + + let response = app + .post_login(&serde_json::json!({ + "email": email, + "password": password, + })) + .await; + + assert_eq!(response.status().as_u16(), 200); + + let auth_cookie = response + .cookies() + .find(|cookie| cookie.name() == JWT_COOKIE_NAME) + .expect("No auth cookie found"); + + assert!(!auth_cookie.value().is_empty()); + + let token = auth_cookie.value(); + + assert_eq!(app.post_logout().await.status().as_u16(), 200); + + let contains_banned_token = app + .banned_tokens_store + .read() + .await + .contains_token(token) + .await + .expect("Failed to check banned token"); + + assert_eq!(contains_banned_token, true); +} diff --git a/auth-service/tests/api/main.rs b/auth-service/tests/api/main.rs new file mode 100644 index 000000000..6294c1360 --- /dev/null +++ b/auth-service/tests/api/main.rs @@ -0,0 +1,7 @@ +mod helpers; +mod login; +mod logout; +mod root; +mod signup; +mod verify_2fa; +mod verify_token; diff --git a/auth-service/tests/api/root.rs b/auth-service/tests/api/root.rs new file mode 100644 index 000000000..92a80f44d --- /dev/null +++ b/auth-service/tests/api/root.rs @@ -0,0 +1,13 @@ +use crate::helpers::TestApp; +use test_macros::auto_db_cleanup; + +#[auto_db_cleanup] +#[tokio::test] +async fn root_returns_auth_ui() { + let mut app = TestApp::new().await; + + let response = app.get_root().await; + + assert_eq!(response.status().as_u16(), 200); + assert_eq!(response.headers().get("content-type").unwrap(), "text/html"); +} diff --git a/auth-service/tests/api/signup.rs b/auth-service/tests/api/signup.rs new file mode 100644 index 000000000..dc6cf1be0 --- /dev/null +++ b/auth-service/tests/api/signup.rs @@ -0,0 +1,145 @@ +use crate::helpers::{self, TestApp}; + +use auth_service::{ErrorResponse, routes::SignupResponse}; +use test_macros::auto_db_cleanup; + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_400_if_invalid_input() { + let mut app = TestApp::new().await; + + let test_cases = [ + serde_json::json!({ + "password": "12345678", + "email":"user_email.test", + "requires2FA": false + }), + serde_json::json!({ + "password": "123456", + "email":"user@email.test", + "requires2FA": false + }), + ]; + + for test_case in test_cases.iter() { + let response = app.post_signup(test_case).await; + assert_eq!( + response.status().as_u16(), + 400, + "Failed for input: {:?}", + test_case + ); + + assert_eq!( + response + .json::() + .await + .expect("Could not deserialize response body to ErrorResponse") + .error, + "Invalid credentials".to_owned() + ); + } +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_409_if_email_already_exists() { + let mut app = TestApp::new().await; + + let test_user = serde_json::json!({ + "password": "password123", + "email":"user@email.test", + "requires2FA": false + }); + let response = app.post_signup(&test_user).await; + + assert_eq!(response.status().as_u16(), 201); + // check if test fails + // let test_user = serde_json::json!({ + // "password": "password123", + // "email":"user2@email.test", + // "requires2FA": false + // }); + + let response = app.post_signup(&test_user).await; + + assert_eq!(response.status().as_u16(), 409); + + assert_eq!( + response + .json::() + .await + .expect("Could not deserialize response body to ErrorResponse") + .error, + "User already exists".to_owned() + ); +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_201_if_valid_input() { + let mut app = TestApp::new().await; + + let test_case = serde_json::json!({ + "email": helpers::get_random_email(), + "password": "password123", + "requires2FA": true + }); + + let response = app.post_signup(&test_case).await; + + assert_eq!(response.status().as_u16(), 201); + + let expected_response = SignupResponse { + message: "User created successfully!".to_owned(), + }; + + assert_eq!( + response + .json::() + .await + .expect("Could not deserialize response body to UserBody"), + expected_response + ); +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_422_if_malformed_input() { + let random_email = helpers::get_random_email(); // Call helper method to generate email + + let test_cases = [ + serde_json::json!({ + "password": "password123", + "requires2FA": true + }), + serde_json::json!({ + "email":random_email, + "requires2FA": true + }), + serde_json::json!({ + "password": "password123", + "email":random_email + }), + serde_json::json!({ + "password": "password123", + "email":true, + "requires2FA": 1000 + }), + ]; + + let mut app = TestApp::new().await; + + for test_case in test_cases.iter() { + let response = app.post_signup(&test_case).await; + + println!("\x1b[55m response.status:'{:?}' \x1b[0m", response.status()); + + assert_eq!( + response.status().as_u16(), + 422, + "Failed for input: {:?}", + test_case + ); + } +} diff --git a/auth-service/tests/api/verify_2fa.rs b/auth-service/tests/api/verify_2fa.rs new file mode 100644 index 000000000..d359928a1 --- /dev/null +++ b/auth-service/tests/api/verify_2fa.rs @@ -0,0 +1,466 @@ +use auth_service::domain::data_stores::TwoFACodeStore; + +use auth_service::{ + domain::email::Email, routes::TwoFactorAuthResponse, utils::constants::JWT_COOKIE_NAME, +}; + +use crate::helpers::TestApp; +use auth_service::domain::data_stores::TwoFACode; +use test_macros::auto_db_cleanup; + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_401_if_same_code_twice() { + let mut app = TestApp::new().await; + + let email = "example@email.test"; + let password = "12345678"; + { + //create new User + let signup_body = serde_json::json!({ + "email": email, + "password": password, + "requires2FA": true + }); + + assert_eq!(app.post_signup(&signup_body).await.status().as_u16(), 201); + } + + let response = app + .post_login(&serde_json::json!({ + "email": email, + "password": password + })) + .await; + + assert_eq!(response.status().as_u16(), 206); + + let auth_cookie = response + .cookies() + .find(|cookie| cookie.name() == JWT_COOKIE_NAME) + .expect("No auth cookie found"); + + assert!(!auth_cookie.value().is_empty()); + + let response_body = response + .json::() + .await + .expect("Could not deserialize response body to TwoFactorAuthResponse"); + + assert_eq!(response_body.message, "2FA required".to_owned()); + assert!(!response_body.login_attempt_id.is_empty()); + + let login_attempt_id = response_body.login_attempt_id; + + let code_tuple = app + .two_fa_code_store + .read() + .await + .get_code(&Email::parse(email.to_owned()).unwrap()) + .await + .unwrap(); + + let code = code_tuple.1.as_ref(); + + let response = app + .post_verify2fa(&serde_json::json!({ + "email": email, + "loginAttemptId": login_attempt_id, + "2FACode": code + })) + .await; + + assert_eq!(response.status().as_u16(), 200); + + let response = app + .post_verify2fa(&serde_json::json!({ + "email": email, + "loginAttemptId": login_attempt_id, + "2FACode": code + })) + .await; + assert_eq!(response.status().as_u16(), 401); +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_401_if_incorrect_credentials() { + let mut app = TestApp::new().await; + + let email = "example@email.test"; + let password = "12345678"; + { + //create new User + let signup_body = serde_json::json!({ + "email": email, + "password": password, + "requires2FA": true + }); + + assert_eq!(app.post_signup(&signup_body).await.status().as_u16(), 201); + } + + let response_login = app + .post_login(&serde_json::json!({ + "email": email, + "password": password + })) + .await; + + assert_eq!(response_login.status().as_u16(), 206); + + let response_body = response_login + .json::() + .await + .expect("Could not deserialize response body to TwoFactorAuthResponse"); + + assert_eq!(response_body.message, "2FA required".to_owned()); + assert!(!response_body.login_attempt_id.is_empty()); + + { + let two_fa_code = TwoFACode::default().as_ref().to_owned(); + + let test_cases = &[ + serde_json::json!({ + "email": email, + "loginAttemptId":response_body.login_attempt_id, + "2FACode": "123456" + }), + serde_json::json!({ + "email": email, + "loginAttemptId":response_body.login_attempt_id, + "2FACode": two_fa_code + }), + ]; + + for test_case in test_cases { + let verify_2fa_rsponse = app.post_verify2fa(&test_case).await; + + assert_eq!( + verify_2fa_rsponse.status().as_u16(), + 401, + "\x1b[41m verify_2fa_response: {:?} \x1b[0m", + verify_2fa_rsponse + ); + } + } +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_401_if_old_code() { + // Call login twice. Then, attempt to call verify-fa with the 2FA code from the first login requet. This should fail. + let mut app = TestApp::new().await; + + let email = "example@email.test"; + let password = "12345678"; + { + //create new User + let signup_body = serde_json::json!({ + "email": email, + "password": password, + "requires2FA": true + }); + + assert_eq!(app.post_signup(&signup_body).await.status().as_u16(), 201); + } + + let response = app + .post_login(&serde_json::json!({ + "email": email, + "password": password + })) + .await; + + assert_eq!(response.status().as_u16(), 206); + + let auth_cookie = response + .cookies() + .find(|cookie| cookie.name() == JWT_COOKIE_NAME) + .expect("No auth cookie found"); + + assert!(!auth_cookie.value().is_empty()); + + { + let response = app + .post_login(&serde_json::json!({ + "email": email, + "password": password + })) + .await; + + assert_eq!(response.status().as_u16(), 206); + } + + let response_body = response + .json::() + .await + .expect("Could not deserialize response body to TwoFactorAuthResponse"); + + assert_eq!(response_body.message, "2FA required".to_owned()); + assert!(!response_body.login_attempt_id.is_empty()); + + let login_attempt_id = response_body.login_attempt_id; + + let code_tuple = app + .two_fa_code_store + .read() + .await + .get_code(&Email::parse(email.to_owned()).unwrap()) + .await + .unwrap(); + + let code = code_tuple.1.as_ref(); + + let response = app + .post_verify2fa(&serde_json::json!({ + "email": email, + "loginAttemptId": login_attempt_id, + "2FACode": code + })) + .await; + + assert_eq!(response.status().as_u16(), 401); + // assert_eq!(response.status().as_u16(), 200); +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_400_if_invalid_input() { + let mut app = TestApp::new().await; + + let email = "example@email.test"; + let password = "12345678"; + { + //create new User + let signup_body = serde_json::json!({ + "email": email, + "password": password, + "requires2FA": true + }); + + assert_eq!(app.post_signup(&signup_body).await.status().as_u16(), 201); + } + + let response_login = app + .post_login(&serde_json::json!({ + "email": email, + "password": password + })) + .await; + + assert_eq!(response_login.status().as_u16(), 206); + + let response_body = response_login + .json::() + .await + .expect("Could not deserialize response body to TwoFactorAuthResponse"); + + assert_eq!(response_body.message, "2FA required".to_owned()); + assert!(!response_body.login_attempt_id.is_empty()); + + { + let two_fa_code = TwoFACode::default().as_ref().to_owned(); + + let test_cases = &[ + serde_json::json!({ + "email": email, + "loginAttemptId":response_body.login_attempt_id, + "2FACode": "" + }), + serde_json::json!({ + "email": email, + "loginAttemptId":"", + "2FACode": "" + }), + serde_json::json!({ + "email": email, + "loginAttemptId":"12345678901234567890", + "2FACode": two_fa_code + }), + serde_json::json!({ + "email": email, + "loginAttemptId":"", + "2FACode": "123456" + }), + // serde_json::json!({ + // "email": email, + // "loginAttemptId":response_body.login_attempt_id, + // "2FACode": "123456" + // }), + ]; + + for test_case in test_cases { + let verify_2fa_rsponse = app.post_verify2fa(&test_case).await; + + assert_eq!( + verify_2fa_rsponse.status().as_u16(), + 400, + "\x1b[41m verify_2fa_response: {:?} \x1b[0m", + verify_2fa_rsponse + ); + } + } +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_422_if_malformed_input() { + let mut app = TestApp::new().await; + + let email = "example@email.test"; + let password = "12345678"; + { + //create new User + let signup_body = serde_json::json!({ + "email": email, + "password": password, + "requires2FA": true + }); + + assert_eq!(app.post_signup(&signup_body).await.status().as_u16(), 201); + } + + let response_login = app + .post_login(&serde_json::json!({ + "email": email, + "password": password + })) + .await; + + assert_eq!(response_login.status().as_u16(), 206); + + let response_body = response_login + .json::() + .await + .expect("Could not deserialize response body to TwoFactorAuthResponse"); + + assert_eq!(response_body.message, "2FA required".to_owned()); + assert!(!response_body.login_attempt_id.is_empty()); + + { + let test_cases = &[ + serde_json::json!({ + "email": email, + "loginAttemptId": response_body.login_attempt_id, + "2FACode": 1111 + }), + serde_json::json!({ + "email": email, + "loginAttemptId": response_body.login_attempt_id, + // "2FACode": "string" + }), + serde_json::json!({ + "email": email, + // "loginAttemptId": response_body.login_attempt_id, + "2FACode": "string" + }), + serde_json::json!({ + "email": true, + "loginAttemptId":response_body.login_attempt_id, + "2FACode": "" + }), + serde_json::json!({ + "email": email, + "loginAttemptId": {}, + "2FACode": "string" + }), + serde_json::json!({ + "2FACode": "123456", + }), + serde_json::json!({ + "email": email, + }), + serde_json::json!({ + "loginAttemptId": response_body.login_attempt_id, + }), + serde_json::json!({ + "2FACode": "123456", + "email": email, + }), + serde_json::json!({ + "2FACode": "123456", + "loginAttemptId": response_body.login_attempt_id, + }), + serde_json::json!({ + "email": email, + "loginAttemptId": response_body.login_attempt_id, + }), + serde_json::json!({}), + ]; + + for test_case in test_cases { + let verify_2fa_rsponse = app.post_verify2fa(&test_case).await; + + assert_eq!( + verify_2fa_rsponse.status().as_u16(), + 422, + "\x1b[41m verify_2fa_response: {:?} \x1b[0m", + verify_2fa_rsponse + ); + } + } +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_200_if_correct_code() { + let mut app = TestApp::new().await; + + let email = "example@email.test"; + let password = "12345678"; + { + //create new User + let signup_body = serde_json::json!({ + "email": email, + "password": password, + "requires2FA": true + }); + + assert_eq!(app.post_signup(&signup_body).await.status().as_u16(), 201); + } + + let response_login = app + .post_login(&serde_json::json!({ + "email": email, + "password": password + })) + .await; + + assert_eq!(response_login.status().as_u16(), 206); + + { + let auth_cookie = response_login + .cookies() + .find(|cookie| cookie.name() == JWT_COOKIE_NAME) + .expect("No auth cookie found"); + + assert!(!auth_cookie.value().is_empty()); + } + + let response_body = response_login + .json::() + .await + .expect("Could not deserialize response body to TwoFactorAuthResponse"); + + assert_eq!(response_body.message, "2FA required".to_owned()); + assert!(!response_body.login_attempt_id.is_empty()); + + let code_tuple = app + .two_fa_code_store + .read() + .await + .get_code(&Email::parse(email.to_owned()).unwrap()) + .await + .unwrap(); + + let response = app + .post_verify2fa(&serde_json::json!({ + "email": email, + "loginAttemptId": response_body.login_attempt_id, + "2FACode": code_tuple.1.as_ref() + })) + .await; + + assert_eq!(response.status().as_u16(), 200); +} diff --git a/auth-service/tests/api/verify_token.rs b/auth-service/tests/api/verify_token.rs new file mode 100644 index 000000000..640026424 --- /dev/null +++ b/auth-service/tests/api/verify_token.rs @@ -0,0 +1,156 @@ +use crate::helpers::TestApp; +use auth_service::{domain::data_stores::BannedTokenStore, utils::constants::JWT_COOKIE_NAME}; +use test_macros::auto_db_cleanup; + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_200_valid_token() { + let mut app = TestApp::new().await; + + let email = "example@email.test"; + let password = "12345678"; + + { + //create new User + let test_case = serde_json::json!({ + "email": email, + "password": password, + "requires2FA": false + }); + + assert_eq!(app.post_signup(&test_case).await.status().as_u16(), 201); + } + + let response = app + .post_login(&serde_json::json!({ + "email": email, + "password": password, + })) + .await; + + assert_eq!(response.status().as_u16(), 200); + + let auth_cookie = response + .cookies() + .find(|cookie| cookie.name() == auth_service::utils::constants::JWT_COOKIE_NAME) + .expect("No auth cookie found"); + + let token = auth_cookie.value(); + + assert!(!token.is_empty()); + + { + //verify token + let response = app + .post_verify_token(&serde_json::json!({ + "token": token, + })) + .await; + + assert_eq!(response.status().as_u16(), 200); + } +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_401_if_banned_token() { + let mut app = TestApp::new().await; + + let email = "example@email.test"; + let password = "12345678"; + + assert_eq!( + app.post_signup(&serde_json::json!({ + "email": email, + "password": password, + "requires2FA": false + })) + .await + .status() + .as_u16(), + 201 + ); + + let response = app + .post_login(&serde_json::json!({ + "email": email, + "password": password, + })) + .await; + + assert_eq!(response.status().as_u16(), 200); + + let auth_cookie = response + .cookies() + .find(|cookie| cookie.name() == JWT_COOKIE_NAME) + .expect("No auth cookie found"); + + assert!(!auth_cookie.value().is_empty()); + + let token = auth_cookie.value(); + + assert_eq!(app.post_logout().await.status().as_u16(), 200); + + let contains_banned_token = app + .banned_tokens_store + .read() + .await + .contains_token(token) + .await + .expect("Failed to check banned token"); + + assert_eq!(contains_banned_token, true); + + { + assert_eq!( + app.post_verify_token(&serde_json::json!({ + "token": token, + })) + .await + .status() + .as_u16(), + 401 + ); + } +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_401_if_invalid_token() { + let mut app = TestApp::new().await; + + let result = app + .post_verify_token(&serde_json::json!({ + "token": "invalid", + })) + .await; + + assert_eq!(result.status().as_u16(), 401); +} + +#[auto_db_cleanup] +#[tokio::test] +async fn should_return_422_if_malformed_input() { + let test_cases = &[ + // serde_json::json!({ + // "token":"invalid", + // }), + serde_json::json!({ + "token":true, + }), + serde_json::json!({ + "token":111, + }), + serde_json::json!({ + "token":{}, + }), + serde_json::json!({}), + ]; + + let mut app = TestApp::new().await; + for test_case in test_cases.iter() { + let response = app.post_verify_token(test_case).await; + + assert_eq!(response.status().as_u16(), 422); + } +} diff --git a/compose.override.yml b/compose.override.yml index 9fe36ecb1..35776b651 100644 --- a/compose.override.yml +++ b/compose.override.yml @@ -4,4 +4,4 @@ services: context: ./app-service # specify directory where local Dockerfile is located auth-service: build: - context: ./auth-service # specify directory where local Dockerfile is located \ No newline at end of file + context: ./auth-service # specify directory where local Dockerfile is located diff --git a/compose.yml b/compose.yml index a54c69c4f..6f6571cb0 100644 --- a/compose.yml +++ b/compose.yml @@ -1,18 +1,38 @@ services: app-service: - # TODO: change "letsgetrusty" to your Docker Hub username - image: letsgetrusty/app-service # specify name of image on Docker Hub + image: traianistrati/app-service # specify name of image on Docker Hub restart: "always" # automatically restart container when server crashes environment: # set up environment variables AUTH_SERVICE_IP: ${AUTH_SERVICE_IP:-localhost} # Use localhost as the default value ports: - - "8000:8000" # expose port 8000 so that applications outside the container can connect to it + - "8000:8000" # expose port 8000 so that applications outside the container can connect to it depends_on: # only run app-service after auth-service has started auth-service: condition: service_started auth-service: - # TODO: change "letsgetrusty" to your Docker Hub username - image: letsgetrusty/auth-service + image: traianistrati/auth-service restart: "always" # automatically restart container when server crashes + environment: + JWT_SECRET: ${JWT_SECRET} + DATABASE_URL: "postgres://postgres:${POSTGRES_PASSWORD}@db:5432" ports: - - "3000:3000" # expose port 3000 so that applications outside the container can connect to it \ No newline at end of file + - "3000:3000" # expose port 3000 so that applications outside the container can connect to it + depends_on: + - db + db: + image: postgres:15.2-alpine + restart: always + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - "5432:5432" + volumes: + - db:/var/lib/postgresql/data + redis: + image: redis:7.0-alpine + restart: always + ports: + - "6379:6379" +volumes: + db: + driver: local diff --git a/docker.bat b/docker.bat new file mode 100644 index 000000000..08a75d47b --- /dev/null +++ b/docker.bat @@ -0,0 +1,15 @@ +@echo off + +set ENV_FILE=.\auth-service\.env + +if not exist "%ENV_FILE%" ( + echo Error: .env file not found! + exit /b 1 +) + +for /f "usebackq tokens=*" %%i in ("%ENV_FILE%") do ( + set %%i +) + +docker compose build +docker compose up diff --git a/docker.sh b/docker.sh new file mode 100644 index 000000000..a2231a182 --- /dev/null +++ b/docker.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Define the location of the .env file (change if needed) +ENV_FILE="./auth-service/.env" + +# Check if the .env file exists +if ! [[ -f "$ENV_FILE" ]]; then + echo "Error: .env file not found!" + exit 1 +fi + +# Read each line in the .env file (ignoring comments) +while IFS= read -r line; do + # Skip blank lines and lines starting with # + if [[ -n "$line" ]] && [[ "$line" != \#* ]]; then + # Split the line into key and value + key=$(echo "$line" | cut -d '=' -f1) + value=$(echo "$line" | cut -d '=' -f2-) + # Export the variable + export "$key=$value" + fi +done < <(grep -v '^#' "$ENV_FILE") + +# Run docker-compose commands with exported variables +docker-compose build +docker-compose up