From 17fbe1d507aca2c4d0f41ba296fc1ef9dfdd8ba8 Mon Sep 17 00:00:00 2001 From: Lucien Cartier-Tilet Date: Thu, 13 Nov 2025 23:28:01 +0100 Subject: [PATCH] chore: separate frontend from backend --- .devenv-root | 2 +- .env.example | 9 - .envrc | 36 +- .gitattributes | 1 - .github/workflows/README.md | 217 -- .github/workflows/publish-docker.yml | 123 - .gitignore | 4 - frontend/.prettierrc => .prettierrc | 0 .volarrc | 9 - README.org | 176 +- {frontend/app => app}/app.vue | 0 {frontend/app => app}/assets/css/colors.css | 0 {frontend/app => app}/assets/css/main.css | 0 {frontend/app => app}/assets/css/tailwind.css | 0 .../app => app}/assets/css/ui/background.css | 0 .../app => app}/assets/css/ui/border.css | 0 .../app => app}/assets/css/ui/colors.css | 0 {frontend/app => app}/assets/css/ui/index.css | 0 {frontend/app => app}/assets/css/ui/text.css | 0 .../app => app}/components/AppFooter.vue | 0 .../app => app}/components/AppNavbar.vue | 0 .../app => app}/components/Ui/BadgeList.vue | 0 .../components/Ui/BadgeListCard.vue | 0 .../components/VocalSynth/Projects.vue | 0 .../components/VocalSynth/Tools.vue | 0 .../components/navbar/LanguageSwitcher.vue | 0 .../components/navbar/ThemeSwitcher.vue | 0 {frontend/app => app}/composables/useApi.ts | 0 .../app => app}/composables/useBackend.ts | 0 .../app => app}/composables/useDataJson.ts | 1 - {frontend/app => app}/composables/useMeta.ts | 0 {frontend/app => app}/layouts/centered.vue | 0 {frontend/app => app}/layouts/default.vue | 0 {frontend/app => app}/pages/[...slug].vue | 0 {frontend/app => app}/pages/resume.vue | 0 {frontend/app => app}/types/api/contact.ts | 0 {frontend/app => app}/types/api/error.ts | 0 {frontend/app => app}/types/api/meta.ts | 0 {frontend/app => app}/types/dictionary.ts | 0 {frontend/app => app}/types/resume.ts | 0 {frontend/app => app}/types/tool.ts | 0 backend/.tarpaulin.ci.toml | 6 - backend/.tarpaulin.local.toml | 7 - backend/Cargo.lock | 3249 ----------------- backend/Cargo.toml | 33 - backend/README.md | 424 --- backend/bacon.toml | 84 - backend/deny.toml | 51 - backend/justfile | 48 - backend/nix/package.nix | 60 - backend/nix/rust-version.nix | 6 - backend/nix/shell.nix | 75 - backend/settings/base.yaml | 8 - backend/settings/development.yaml | 18 - backend/settings/production.yaml | 18 - backend/src/lib.rs | 82 - backend/src/main.rs | 7 - backend/src/middleware/mod.rs | 5 - backend/src/middleware/rate_limit.rs | 211 -- backend/src/route/contact.rs | 514 --- backend/src/route/health.rs | 38 - backend/src/route/meta.rs | 86 - backend/src/route/mod.rs | 46 - backend/src/settings.rs | 619 ---- backend/src/startup.rs | 228 -- backend/src/telemetry.rs | 69 - .../content.config.ts => content.config.ts | 0 {frontend/content => content}/en/index.md | 0 {frontend/content => content}/en/resume.json | 0 .../en/vocal-synthesis.json | 0 .../content => content}/en/vocal-synthesis.md | 0 {frontend/content => content}/fr/index.md | 0 {frontend/content => content}/fr/resume.json | 0 .../fr/vocal-synthesis.json | 0 .../content => content}/fr/vocal-synthesis.md | 0 .../eslint.config.mjs => eslint.config.mjs | 0 flake.lock | 49 +- flake.nix | 39 +- frontend/README.org | 134 - frontend/shell.nix | 54 - frontend/i18n.config.ts => i18n.config.ts | 0 {frontend/i18n => i18n}/locales/en.json | 0 {frontend/i18n => i18n}/locales/fr.json | 0 nix/shell.nix | 33 + frontend/nuxt.config.ts => nuxt.config.ts | 0 frontend/package.json => package.json | 9 +- frontend/pnpm-lock.yaml => pnpm-lock.yaml | 505 ++- ...pnpm-workspace.yaml => pnpm-workspace.yaml | 0 {frontend/public => public}/favicon.ico | Bin {frontend/public => public}/robots.txt | 0 frontend/tsconfig.json => tsconfig.json | 7 + 91 files changed, 415 insertions(+), 6985 deletions(-) delete mode 100644 .gitattributes delete mode 100644 .github/workflows/README.md delete mode 100644 .github/workflows/publish-docker.yml rename frontend/.prettierrc => .prettierrc (100%) rename {frontend/app => app}/app.vue (100%) rename {frontend/app => app}/assets/css/colors.css (100%) rename {frontend/app => app}/assets/css/main.css (100%) rename {frontend/app => app}/assets/css/tailwind.css (100%) rename {frontend/app => app}/assets/css/ui/background.css (100%) rename {frontend/app => app}/assets/css/ui/border.css (100%) rename {frontend/app => app}/assets/css/ui/colors.css (100%) rename {frontend/app => app}/assets/css/ui/index.css (100%) rename {frontend/app => app}/assets/css/ui/text.css (100%) rename {frontend/app => app}/components/AppFooter.vue (100%) rename {frontend/app => app}/components/AppNavbar.vue (100%) rename {frontend/app => app}/components/Ui/BadgeList.vue (100%) rename {frontend/app => app}/components/Ui/BadgeListCard.vue (100%) rename {frontend/app => app}/components/VocalSynth/Projects.vue (100%) rename {frontend/app => app}/components/VocalSynth/Tools.vue (100%) rename {frontend/app => app}/components/navbar/LanguageSwitcher.vue (100%) rename {frontend/app => app}/components/navbar/ThemeSwitcher.vue (100%) rename {frontend/app => app}/composables/useApi.ts (100%) rename {frontend/app => app}/composables/useBackend.ts (100%) rename {frontend/app => app}/composables/useDataJson.ts (99%) rename {frontend/app => app}/composables/useMeta.ts (100%) rename {frontend/app => app}/layouts/centered.vue (100%) rename {frontend/app => app}/layouts/default.vue (100%) rename {frontend/app => app}/pages/[...slug].vue (100%) rename {frontend/app => app}/pages/resume.vue (100%) rename {frontend/app => app}/types/api/contact.ts (100%) rename {frontend/app => app}/types/api/error.ts (100%) rename {frontend/app => app}/types/api/meta.ts (100%) rename {frontend/app => app}/types/dictionary.ts (100%) rename {frontend/app => app}/types/resume.ts (100%) rename {frontend/app => app}/types/tool.ts (100%) delete mode 100644 backend/.tarpaulin.ci.toml delete mode 100644 backend/.tarpaulin.local.toml delete mode 100644 backend/Cargo.lock delete mode 100644 backend/Cargo.toml delete mode 100644 backend/README.md delete mode 100644 backend/bacon.toml delete mode 100644 backend/deny.toml delete mode 100644 backend/justfile delete mode 100644 backend/nix/package.nix delete mode 100644 backend/nix/rust-version.nix delete mode 100644 backend/nix/shell.nix delete mode 100644 backend/settings/base.yaml delete mode 100644 backend/settings/development.yaml delete mode 100644 backend/settings/production.yaml delete mode 100644 backend/src/lib.rs delete mode 100644 backend/src/main.rs delete mode 100644 backend/src/middleware/mod.rs delete mode 100644 backend/src/middleware/rate_limit.rs delete mode 100644 backend/src/route/contact.rs delete mode 100644 backend/src/route/health.rs delete mode 100644 backend/src/route/meta.rs delete mode 100644 backend/src/route/mod.rs delete mode 100644 backend/src/settings.rs delete mode 100644 backend/src/startup.rs delete mode 100644 backend/src/telemetry.rs rename frontend/content.config.ts => content.config.ts (100%) rename {frontend/content => content}/en/index.md (100%) rename {frontend/content => content}/en/resume.json (100%) rename {frontend/content => content}/en/vocal-synthesis.json (100%) rename {frontend/content => content}/en/vocal-synthesis.md (100%) rename {frontend/content => content}/fr/index.md (100%) rename {frontend/content => content}/fr/resume.json (100%) rename {frontend/content => content}/fr/vocal-synthesis.json (100%) rename {frontend/content => content}/fr/vocal-synthesis.md (100%) rename frontend/eslint.config.mjs => eslint.config.mjs (100%) delete mode 100644 frontend/README.org delete mode 100644 frontend/shell.nix rename frontend/i18n.config.ts => i18n.config.ts (100%) rename {frontend/i18n => i18n}/locales/en.json (100%) rename {frontend/i18n => i18n}/locales/fr.json (100%) create mode 100644 nix/shell.nix rename frontend/nuxt.config.ts => nuxt.config.ts (100%) rename frontend/package.json => package.json (94%) rename frontend/pnpm-lock.yaml => pnpm-lock.yaml (97%) rename frontend/pnpm-workspace.yaml => pnpm-workspace.yaml (100%) rename {frontend/public => public}/favicon.ico (100%) rename {frontend/public => public}/robots.txt (100%) rename frontend/tsconfig.json => tsconfig.json (75%) diff --git a/.devenv-root b/.devenv-root index a925ada..67d806f 100644 --- a/.devenv-root +++ b/.devenv-root @@ -1 +1 @@ -/home/phundrak/code/web/phundrak.com +/home/phundrak/code/web/phundrak.com-frontend diff --git a/.env.example b/.env.example index 106d151..356a7e0 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,3 @@ -APP_ENVIRONMENT=dev -APP__EMAIL__HOST=mail.example.com -APP__EMAIL__PORT=465 -APP__EMAIL__TLS=true -APP__EMAIL__STARTTLS=no -APP__EMAIL__USER="username" -APP__EMAIL__PASSWORD="changeme" -APP__EMAIL__RECIPIENT="Recipient " -APP__EMAIL__FROM="Contact Form " NUXT_PUBLIC_BACKEND_URL=http://localhost:3100 NUXT_PUBLIC_TURNSTILE_SITE_KEY="changeme" NUXT_TURNSTILE_SECRET_KEY="changeme" diff --git a/.envrc b/.envrc index 4f207f0..4727e5d 100644 --- a/.envrc +++ b/.envrc @@ -12,43 +12,13 @@ dotenv_if_exists watch_file flake.nix watch_file flake.lock watch_file .envrc.local -watch_file backend/shell.nix -watch_file frontend/shell.nix +watch_file nix/shell.nix # Check if .envrc.local exists and contains a shell preference if [[ -f .envrc.local ]]; then source .envrc.local fi -# If no shell is specified, prompt the user interactively -if [[ -z "$NIX_SHELL_NAME" ]]; then - echo "" - echo "πŸ”§ Available development shells:" - echo " 1) frontend - Nuxt.js/Vue development environment" - echo " 2) backend - Rust backend development environment" - echo "" - echo "πŸ’‘ Tip: Create a .envrc.local file with 'export NIX_SHELL_NAME=frontend' to skip this prompt" - echo "" - - # Read user input - read -p "Select shell (1 or 2): " choice - - case $choice in - 1|frontend) - NIX_SHELL_NAME=frontend - ;; - 2|backend) - NIX_SHELL_NAME=backend - ;; - *) - echo "❌ Invalid choice. Please select 1 or 2." - return 1 - ;; - esac - - echo "βœ… Loading ${NIX_SHELL_NAME} environment..." -fi - -if ! use flake ".#${NIX_SHELL_NAME}" --no-pure-eval; then - echo "❌ devenv could not be built. The devenv environment was not loaded. Make the necessary changes to flake.nix and hit enter to try again." >&2 +if ! use flake . --no-pure-eval; then + echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to flake.nix and hit enter to try again." >&2 fi diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 4cfc821..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.org linguist-detectable=true diff --git a/.github/workflows/README.md b/.github/workflows/README.md deleted file mode 100644 index b15d698..0000000 --- a/.github/workflows/README.md +++ /dev/null @@ -1,217 +0,0 @@ -# GitHub Actions Workflows - -## Docker Image Publishing - -The `publish-docker.yml` workflow automatically builds and publishes Docker images for the backend service using Nix. - -### Triggers and Tagging Strategy - -| Event | Condition | Published Tags | Example | -|--------------+-----------------------------+------------------------+-------------------| -| Tag push | Tag pushed to `main` branch | `latest` + version tag | `latest`, `1.0.0` | -| Branch push | Push to `develop` branch | `develop` | `develop` | -| Pull request | PR opened or updated | `pr` | `pr12` | -| Branch push | Push to `main` (no tag) | `latest` | `latest` | - -### Required Secrets - -Configure these secrets in your repository settings (`Settings` β†’ `Secrets and variables` β†’ `Actions`): - -| Secret Name | Description | Example Value | -|---------------------+---------------------------------------------+-----------------------------------------| -| `DOCKER_USERNAME` | Username for Docker registry authentication | `phundrak` | -| `DOCKER_PASSWORD` | Password or token for Docker registry | Personal Access Token (PAT) or password | -| `CACHIX_AUTH_TOKEN` | (Optional) Token for Cachix caching | Your Cachix auth token | - -#### For GitHub Container Registry (ghcr.io) - -1. Create a Personal Access Token (PAT): - - Go to GitHub Settings β†’ Developer settings β†’ Personal access tokens β†’ Tokens (classic) - - Click "Generate new token (classic)" - - Select scopes: `write:packages`, `read:packages`, `delete:packages` - - Copy the generated token - -2. Add secrets: - - `DOCKER_USERNAME`: Your GitHub username - - `DOCKER_PASSWORD`: The PAT you just created - -#### For Docker Hub - -1. Create an access token: - - Go to Docker Hub β†’ Account Settings β†’ Security β†’ Access Tokens - - Click "New Access Token" - - Set permissions to "Read, Write, Delete" - - Copy the generated token - -2. Add secrets: - - `DOCKER_USERNAME`: Your Docker Hub username - - `DOCKER_PASSWORD`: The access token you just created - -#### For Gitea Registry (e.g., labs.phundrak.com) - -1. Create an access token in Gitea: - - Log in to your Gitea instance - - Go to Settings (click your avatar β†’ Settings) - - Navigate to Applications β†’ Manage Access Tokens - - Click "Generate New Token" - - Give it a descriptive name (e.g., "Phundrak Labs Docker Registry") - - Select the required permissions: - - `write:package` - Required to publish packages - - `read:package` - Required to pull packages - - Click "Generate Token" - - Copy the generated token immediately (it won't be shown again) - -2. Add secrets: - - `DOCKER_USERNAME`: Your Gitea username - - `DOCKER_PASSWORD`: The access token you just created - -Note: Gitea's container registry is accessed at `https://your-gitea-instance/username/-/packages` - -#### For Other Custom Registries - -1. Obtain credentials from your registry administrator - -2. Add secrets: - - `DOCKER_USERNAME`: Your registry username - - `DOCKER_PASSWORD`: Your registry password or token - -### Configuring Cachix (Build Caching) - -Cachix is a Nix binary cache that dramatically speeds up builds by caching build artifacts. The workflow supports configurable Cachix settings. - -#### Environment Variables - -Configure these in the workflow's `env` section or as repository variables: - -| Variable | Description | Default Value | Example | -|--------------------+------------------------------------------------+---------------+--------------------| -| `CACHIX_NAME` | Name of the Cachix cache to use | `devenv` | `phundrak-dot-com` | -| `CACHIX_SKIP_PUSH` | Whether to skip pushing artifacts to the cache | `true` | `false` | - -#### Option 1: Pull from Public Cache Only - -If you only want to pull from a public cache (no pushing): - -1. Set environment variables in the workflow: - ```yaml - env: - CACHIX_NAME: devenv # or any public cache name - CACHIX_SKIP_PUSH: true - ``` - -2. No `CACHIX_AUTH_TOKEN` secret is needed - -This is useful when using public caches like `devenv` or `nix-community`. - -#### Option 2: Use Your Own Cache (Recommended for Faster Builds) - -To cache your own build artifacts for faster subsequent builds: - -1. Create a Cachix cache: - - Go to https://app.cachix.org - - Sign up and create a new cache (e.g., `your-project-name`) - - Free for public/open-source projects - -2. Get your auth token: - - In Cachix, go to your cache settings - - Find your auth token under "Auth tokens" - - Copy the token - -3. Add your cache configuration to `flake.nix`: - ```nix - nixConfig = { - extra-trusted-public-keys = [ - "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=" - "your-cache-name.cachix.org-1:YOUR_PUBLIC_KEY_HERE" - ]; - extra-substituters = [ - "https://devenv.cachix.org" - "https://your-cache-name.cachix.org" - ]; - }; - ``` - -4. Configure the workflow: - - Edit `.github/workflows/publish-docker.yml`: - ```yaml - env: - CACHIX_NAME: your-cache-name - CACHIX_SKIP_PUSH: false - ``` - - Or set as repository variables in GitHub/Gitea - -5. Add your auth token as a secret: - - Go to repository `Settings` β†’ `Secrets and variables` β†’ `Actions` - - Add secret `CACHIX_AUTH_TOKEN` with your token - -#### Benefits of Using Your Own Cache - -- **Faster builds**: Subsequent builds reuse cached artifacts (Rust dependencies, compiled binaries) -- **Reduced CI time**: Can reduce build time from 10+ minutes to under 1 minute -- **Cost savings**: Less compute time means lower CI costs -- **Shared across branches**: All branches benefit from the same cache - -### Configuring the Docker Registry - -The target registry is set via the `DOCKER_REGISTRY` environment variable in the workflow file. To change it: - -1. Edit `.github/workflows/publish-docker.yml` -2. Modify the `env` section: - -```yaml -env: - DOCKER_REGISTRY: ghcr.io # Change to your registry (e.g., docker.io, labs.phundrak.com) - IMAGE_NAME: phundrak/phundrak-dot-com-backend -``` - -Or set it as a repository variable: -- Go to `Settings` β†’ `Secrets and variables` β†’ `Actions` β†’ `Variables` tab -- Add `DOCKER_REGISTRY` with your desired registry URL - -### Image Naming - -Images are published with the name: `${DOCKER_REGISTRY}/${IMAGE_NAME}:${TAG}` - -For example: -- `labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest` -- `labs.phundrak.com/phundrak/phundrak-dot-com-backend:1.0.0` -- `labs.phundrak.com/phundrak/phundrak-dot-com-backend:develop` -- `labs.phundrak.com/phundrak/phundrak-dot-com-backend:pr12` - -### Local Testing - -To test the Docker image build locally: - -```bash -# Build the image with Nix -nix build .#backendDockerLatest - -# Load it into Docker -docker load < result - -# Run the container (image name comes from Cargo.toml package.name) -docker run -p 3100:3100 phundrak/phundrak-dot-com-backend:latest -``` - -### Troubleshooting - -#### Authentication Failures - -If you see authentication errors: -1. Verify your `DOCKER_USERNAME` and `DOCKER_PASSWORD` secrets are correct -2. For ghcr.io, ensure your PAT has the correct permissions -3. Check that the `DOCKER_REGISTRY` matches your credentials - -#### Build Failures - -If the Nix build fails: -1. Test the build locally first: `nix build .#backendDockerLatest` -2. Check the GitHub Actions logs for specific error messages -3. Ensure all dependencies in `flake.nix` are correctly specified - -#### Image Not Appearing in Registry - -1. Verify the workflow completed successfully in the Actions tab -2. Check that the registry URL is correct -3. For ghcr.io, images appear at: `https://github.com/users/USERNAME/packages/container/IMAGE_NAME` -4. Ensure your token has write permissions diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml deleted file mode 100644 index 2ad3e3c..0000000 --- a/.github/workflows/publish-docker.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: Publish Docker Images - -on: - push: - branches: - - main - - develop - tags: - - 'v*.*.*' - pull_request: - types: [opened, synchronize, reopened] - -env: - CACHIX_NAME: devenv - CACHIX_SKIP_PUSH: true - DOCKER_REGISTRY: labs.phundrak.com # Override in repository settings if needed - IMAGE_NAME: phundrak/phundrak-dot-com-backend - -jobs: - build-and-publish: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write # Required for pushing to Phundrak Labs registry - pull-requests: read - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install Nix - uses: cachix/install-nix-action@v27 - with: - nix_path: nixpkgs=channel:nixos-unstable - - - name: Setup Cachix - uses: cachix/cachix-action@v15 - with: - name: '${{ env.CACHIX_NAME }}' - authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' - skipPush: ${{ env.CACHIX_SKIP_PUSH }} - - - name: Build Docker image with Nix - run: | - echo "Building Docker image..." - nix build .#backendDockerLatest --accept-flake-config - - - name: Load Docker image - run: | - echo "Loading Docker image into Docker daemon..." - docker load < result - - - name: Log in to Docker Registry - run: | - echo "${{ secrets.DOCKER_PASSWORD }}" | docker login ${{ env.DOCKER_REGISTRY }} -u ${{ secrets.DOCKER_USERNAME }} --password-stdin - - - name: Determine tags and push images - run: | - set -euo pipefail - - REGISTRY="${{ env.DOCKER_REGISTRY }}" - IMAGE_NAME="${{ env.IMAGE_NAME }}" - - # The locally built image from Nix (name comes from Cargo.toml package.name) - LOCAL_IMAGE="phundrak/phundrak-dot-com-backend:latest" - - echo "Event: ${{ github.event_name }}" - echo "Ref: ${{ github.ref }}" - echo "Ref type: ${{ github.ref_type }}" - - # Determine which tags to push based on the event - if [[ "${{ github.event_name }}" == "push" && "${{ github.ref_type }}" == "tag" ]]; then - # Tag push on main branch β†’ publish 'latest' and versioned tag - echo "Tag push detected" - TAG_VERSION="${{ github.ref_name }}" - # Remove 'v' prefix if present (v1.0.0 β†’ 1.0.0) - TAG_VERSION="${TAG_VERSION#v}" - - echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:latest" - docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest" - docker push "${REGISTRY}/${IMAGE_NAME}:latest" - - echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:${TAG_VERSION}" - docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:${TAG_VERSION}" - docker push "${REGISTRY}/${IMAGE_NAME}:${TAG_VERSION}" - - elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/develop" ]]; then - # Push on develop branch β†’ publish 'develop' tag - echo "Push to develop branch detected" - - echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:develop" - docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:develop" - docker push "${REGISTRY}/${IMAGE_NAME}:develop" - - elif [[ "${{ github.event_name }}" == "pull_request" ]]; then - # Pull request β†’ publish 'pr' tag - echo "Pull request detected" - PR_NUMBER="${{ github.event.pull_request.number }}" - - echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:pr${PR_NUMBER}" - docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:pr${PR_NUMBER}" - docker push "${REGISTRY}/${IMAGE_NAME}:pr${PR_NUMBER}" - - elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then - # Push to main branch (not a tag) β†’ publish 'latest' - echo "Push to main branch detected" - - echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:latest" - docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest" - docker push "${REGISTRY}/${IMAGE_NAME}:latest" - - else - echo "Unknown event or ref, skipping push" - exit 1 - fi - - - name: Log out from Docker Registry - if: always() - run: docker logout ${{ env.DOCKER_REGISTRY }} - - - name: Image published successfully - run: | - echo "βœ… Docker image(s) published successfully to ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}" diff --git a/.gitignore b/.gitignore index 7eb6a5d..48a3293 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,6 @@ logs .env.* !.env.example -# Backend -target/ -coverage/ - # Frontend ## Nuxt dev/build outputs .output diff --git a/frontend/.prettierrc b/.prettierrc similarity index 100% rename from frontend/.prettierrc rename to .prettierrc diff --git a/.volarrc b/.volarrc index 1731d06..e69de29 100644 --- a/.volarrc +++ b/.volarrc @@ -1,9 +0,0 @@ -{ - "vueCompilerOptions": { - "target": 3.5, - "extensions": [".vue"] - }, - "typescript": { - "tsdk": "frontend/node_modules/typescript/lib" - } -} diff --git a/README.org b/README.org index c921136..caea59a 100644 --- a/README.org +++ b/README.org @@ -1,76 +1,134 @@ -#+title: phundrak.com +#+title: phundrak.com frontend +#+author: Lucien Cartier-Tilet +#+email: lucien@phundrak.com -#+html: -#+html: -#+html: -#+html: +This is the frontend of =phundrak.com=, written with Nuxt. -* Introduction -This is the repository for my website [[https://phundrak.com][phundrak.com]] which contains the -code available on the =main= branch. Code available on the =develop= -branch is available at [[https://beta.phundrak.com][beta.phundrak.com]]. +* Setup -* Architecture -The website follows a modern full-stack architecture: +** Environment +*** Nix Environment +If you use Nix, you can set up your environment using the [[file:flake.nix][=flake.nix=]] +file, which will give you the exact same development environment as I +use. -- *Backend*: Rust using the [[https://github.com/poem-web/poem][Poem]] web framework (located in [[file:backend/][backend/]]) -- *Frontend*: Nuxt 4 + Vue 3 + TypeScript (located in [[file:frontend/][frontend/]]) - -** Backend -The backend is written in Rust and provides a RESTful API using the -Poem framework with OpenAPI support. - -*** Running the Backend -To run the backend in development mode: -#+begin_src shell -cd backend -cargo run +#+begin_src bash +nix develop #+end_src -To run tests: -#+begin_src shell -cd backend -cargo test +If you have [[https://direnv.net/][=direnv=]] installed, you can simply use it to automatically +enable this environment. However, I *strongly* recommend you to read the +content of the =flake.nix= file before doing so, as you should with any +Nix-defined environment you did not create. + +#+begin_src bash +direnv allow . #+end_src -For continuous testing and linting during development, use [[https://dystroy.org/bacon/][bacon]]: -#+begin_src shell -cd backend -bacon +*** Required Tools +To be able to work on this project, you need a Javascript package +manager, such as: +- =npm= +- =pnpm= (recommended) +- =yarn= +- =bun= + +In my case, I use pnpm. + +You can skip this if you are already using my Nix environment. + +** Dependencies +Once you have your environment ready, you can now install the +project’s dependencies. + +#+begin_src bash +# npm +npm install + +# pnpm +pnpm install + +# yarn +yarn install + +# bun +bun install #+end_src -*** Building the Backend -To build the backend for production: -#+begin_src shell -cd backend -cargo build --release +* Running the Project +You are now ready to start the development server on +=http://localhost:3000=. + +#+begin_src bash +# npm +npm run dev + +# pnpm +pnpm dev + +# yarn +yarn dev + +# bun +bun run dev #+end_src -The compiled binary will be available at =backend/target/release/backend=. +* Production +Once you are satisfied with the project, you can build the application in production mode. -** Frontend -The frontend is built with Nuxt 4, Vue 3, and TypeScript, providing a -modern single-page application experience. +#+begin_src bash +# npm +npm run build -*** Installing Dependencies -First, install the required dependencies using =pnpm=: -#+begin_src shell -cd frontend +# pnpm +pnpm build + +# yarn +yarn build + +# bun +bun run build +#+end_src + +You can preview locally the production build too. + +#+begin_src bash +# npm +npm run preview + +# pnpm +pnpm preview + +# yarn +yarn preview + +# bun +bun run preview +#+end_src + +Check out the [[https://nuxt.com/docs/getting-started/deployment][deployment documentation]] for more information. + +* Known Issues +** =better-sqlite3= self-registration error +If you encounter an error stating that =better-sqlite3= does not +self-register when running =pnpm run dev=, this is typically caused by +the native module being compiled for a different Node.js version. + +*Solution:* Rebuild the native module for your current Node.js version: + +#+begin_src bash +# Rebuild just better-sqlite3 +pnpm rebuild better-sqlite3 + +# Or rebuild all native modules +pnpm rebuild + +# Or reinstall everything (nuclear option) +rm -rf node_modules pnpm install #+end_src -*** Running the Frontend -To run the frontend in development mode: -#+begin_src shell -cd frontend -pnpm dev -#+end_src - -*** Building the Frontend -To build the frontend for production: -#+begin_src shell -cd frontend -pnpm build -#+end_src - -The compiled version of the website can then be found in =frontend/.output=. +*Why this happens:* =better-sqlite3= contains native C++ code that +needs to be compiled for each specific Node.js version. When you +update Node.js or switch between versions, native modules need to be +rebuilt. diff --git a/frontend/app/app.vue b/app/app.vue similarity index 100% rename from frontend/app/app.vue rename to app/app.vue diff --git a/frontend/app/assets/css/colors.css b/app/assets/css/colors.css similarity index 100% rename from frontend/app/assets/css/colors.css rename to app/assets/css/colors.css diff --git a/frontend/app/assets/css/main.css b/app/assets/css/main.css similarity index 100% rename from frontend/app/assets/css/main.css rename to app/assets/css/main.css diff --git a/frontend/app/assets/css/tailwind.css b/app/assets/css/tailwind.css similarity index 100% rename from frontend/app/assets/css/tailwind.css rename to app/assets/css/tailwind.css diff --git a/frontend/app/assets/css/ui/background.css b/app/assets/css/ui/background.css similarity index 100% rename from frontend/app/assets/css/ui/background.css rename to app/assets/css/ui/background.css diff --git a/frontend/app/assets/css/ui/border.css b/app/assets/css/ui/border.css similarity index 100% rename from frontend/app/assets/css/ui/border.css rename to app/assets/css/ui/border.css diff --git a/frontend/app/assets/css/ui/colors.css b/app/assets/css/ui/colors.css similarity index 100% rename from frontend/app/assets/css/ui/colors.css rename to app/assets/css/ui/colors.css diff --git a/frontend/app/assets/css/ui/index.css b/app/assets/css/ui/index.css similarity index 100% rename from frontend/app/assets/css/ui/index.css rename to app/assets/css/ui/index.css diff --git a/frontend/app/assets/css/ui/text.css b/app/assets/css/ui/text.css similarity index 100% rename from frontend/app/assets/css/ui/text.css rename to app/assets/css/ui/text.css diff --git a/frontend/app/components/AppFooter.vue b/app/components/AppFooter.vue similarity index 100% rename from frontend/app/components/AppFooter.vue rename to app/components/AppFooter.vue diff --git a/frontend/app/components/AppNavbar.vue b/app/components/AppNavbar.vue similarity index 100% rename from frontend/app/components/AppNavbar.vue rename to app/components/AppNavbar.vue diff --git a/frontend/app/components/Ui/BadgeList.vue b/app/components/Ui/BadgeList.vue similarity index 100% rename from frontend/app/components/Ui/BadgeList.vue rename to app/components/Ui/BadgeList.vue diff --git a/frontend/app/components/Ui/BadgeListCard.vue b/app/components/Ui/BadgeListCard.vue similarity index 100% rename from frontend/app/components/Ui/BadgeListCard.vue rename to app/components/Ui/BadgeListCard.vue diff --git a/frontend/app/components/VocalSynth/Projects.vue b/app/components/VocalSynth/Projects.vue similarity index 100% rename from frontend/app/components/VocalSynth/Projects.vue rename to app/components/VocalSynth/Projects.vue diff --git a/frontend/app/components/VocalSynth/Tools.vue b/app/components/VocalSynth/Tools.vue similarity index 100% rename from frontend/app/components/VocalSynth/Tools.vue rename to app/components/VocalSynth/Tools.vue diff --git a/frontend/app/components/navbar/LanguageSwitcher.vue b/app/components/navbar/LanguageSwitcher.vue similarity index 100% rename from frontend/app/components/navbar/LanguageSwitcher.vue rename to app/components/navbar/LanguageSwitcher.vue diff --git a/frontend/app/components/navbar/ThemeSwitcher.vue b/app/components/navbar/ThemeSwitcher.vue similarity index 100% rename from frontend/app/components/navbar/ThemeSwitcher.vue rename to app/components/navbar/ThemeSwitcher.vue diff --git a/frontend/app/composables/useApi.ts b/app/composables/useApi.ts similarity index 100% rename from frontend/app/composables/useApi.ts rename to app/composables/useApi.ts diff --git a/frontend/app/composables/useBackend.ts b/app/composables/useBackend.ts similarity index 100% rename from frontend/app/composables/useBackend.ts rename to app/composables/useBackend.ts diff --git a/frontend/app/composables/useDataJson.ts b/app/composables/useDataJson.ts similarity index 99% rename from frontend/app/composables/useDataJson.ts rename to app/composables/useDataJson.ts index d28bdcc..4382a22 100644 --- a/frontend/app/composables/useDataJson.ts +++ b/app/composables/useDataJson.ts @@ -20,7 +20,6 @@ export const useDataJson = (prefix: string) => { } = {}, ) => { const { useFilter = false, fallbackToEnglish = false, extractMeta = false } = options; - const { data } = await useAsyncData( key.value, async () => { diff --git a/frontend/app/composables/useMeta.ts b/app/composables/useMeta.ts similarity index 100% rename from frontend/app/composables/useMeta.ts rename to app/composables/useMeta.ts diff --git a/frontend/app/layouts/centered.vue b/app/layouts/centered.vue similarity index 100% rename from frontend/app/layouts/centered.vue rename to app/layouts/centered.vue diff --git a/frontend/app/layouts/default.vue b/app/layouts/default.vue similarity index 100% rename from frontend/app/layouts/default.vue rename to app/layouts/default.vue diff --git a/frontend/app/pages/[...slug].vue b/app/pages/[...slug].vue similarity index 100% rename from frontend/app/pages/[...slug].vue rename to app/pages/[...slug].vue diff --git a/frontend/app/pages/resume.vue b/app/pages/resume.vue similarity index 100% rename from frontend/app/pages/resume.vue rename to app/pages/resume.vue diff --git a/frontend/app/types/api/contact.ts b/app/types/api/contact.ts similarity index 100% rename from frontend/app/types/api/contact.ts rename to app/types/api/contact.ts diff --git a/frontend/app/types/api/error.ts b/app/types/api/error.ts similarity index 100% rename from frontend/app/types/api/error.ts rename to app/types/api/error.ts diff --git a/frontend/app/types/api/meta.ts b/app/types/api/meta.ts similarity index 100% rename from frontend/app/types/api/meta.ts rename to app/types/api/meta.ts diff --git a/frontend/app/types/dictionary.ts b/app/types/dictionary.ts similarity index 100% rename from frontend/app/types/dictionary.ts rename to app/types/dictionary.ts diff --git a/frontend/app/types/resume.ts b/app/types/resume.ts similarity index 100% rename from frontend/app/types/resume.ts rename to app/types/resume.ts diff --git a/frontend/app/types/tool.ts b/app/types/tool.ts similarity index 100% rename from frontend/app/types/tool.ts rename to app/types/tool.ts diff --git a/backend/.tarpaulin.ci.toml b/backend/.tarpaulin.ci.toml deleted file mode 100644 index 610e47e..0000000 --- a/backend/.tarpaulin.ci.toml +++ /dev/null @@ -1,6 +0,0 @@ -[all] -out = ["Xml"] -target-dir = "coverage" -output-dir = "coverage" -fail-under = 60 -exclude-files = ["target/*"] diff --git a/backend/.tarpaulin.local.toml b/backend/.tarpaulin.local.toml deleted file mode 100644 index 1170b5c..0000000 --- a/backend/.tarpaulin.local.toml +++ /dev/null @@ -1,7 +0,0 @@ -[all] -out = ["Html", "Lcov"] -skip-clean = true -target-dir = "coverage" -output-dir = "coverage" -fail-under = 60 -exclude-files = ["target/*"] diff --git a/backend/Cargo.lock b/backend/Cargo.lock deleted file mode 100644 index 4730111..0000000 --- a/backend/Cargo.lock +++ /dev/null @@ -1,3249 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array 0.14.9", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[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 = "ar_archive_writer" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" -dependencies = [ - "object", -] - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - -[[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 2.0.108", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-lc-rs" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.108", -] - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array 0.14.9", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" - -[[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.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom 7.1.3", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - -[[package]] -name = "chumsky" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" -dependencies = [ - "hashbrown 0.14.5", - "stacker", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - -[[package]] -name = "config" -version = "0.15.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918" -dependencies = [ - "async-trait", - "convert_case", - "json5", - "pathdiff", - "ron", - "rust-ini", - "serde-untagged", - "serde_core", - "serde_json", - "toml", - "winnow", - "yaml-rust2", -] - -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "tiny-keccak", -] - -[[package]] -name = "convert_case" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" -dependencies = [ - "aes-gcm", - "base64 0.22.1", - "hkdf", - "hmac", - "percent-encoding", - "rand 0.8.5", - "sha2", - "subtle", - "time", - "version_check", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array 0.14.9", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "csrf" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23289d34c5bdde82fc8bd33149be1aebdfd6745a273555a1f57a185b2f0de1ea" -dependencies = [ - "aead", - "aes-gcm", - "byteorder", - "chacha20poly1305", - "chrono", - "data-encoding", - "generic-array 1.3.5", - "hmac", - "rand 0.8.5", - "sha2", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.108", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "dlv-list" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" -dependencies = [ - "const-random", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "email-encoding" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6" -dependencies = [ - "base64 0.22.1", - "memchr", -] - -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "erased-serde" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "futures_codec" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce54d63f8b0c75023ed920d46fd71d0cbbb830b0ee012726b5b4f506fb6dea5b" -dependencies = [ - "bytes 0.5.6", - "futures", - "memchr", - "pin-project", -] - -[[package]] -name = "generic-array" -version = "0.14.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "generic-array" -version = "1.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" -dependencies = [ - "rustversion", - "typenum", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "governor" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be93b4ec2e4710b04d9264c0c7350cdd62a8c20e5e4ac732552ebb8f0debe8eb" -dependencies = [ - "cfg-if", - "dashmap", - "futures-sink", - "futures-timer", - "futures-util", - "getrandom 0.3.4", - "no-std-compat", - "nonzero_ext", - "parking_lot", - "portable-atomic", - "quanta", - "rand 0.9.2", - "smallvec", - "spinning_top", - "web-time", -] - -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes 1.10.1", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "headers" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" -dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" -dependencies = [ - "http", -] - -[[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 = "hostname" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" -dependencies = [ - "cfg-if", - "libc", - "windows-link 0.1.3", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes 1.10.1", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes 1.10.1", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes 1.10.1", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes 1.10.1", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "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 1.10.1", - "futures-core", - "http", - "http-body", - "hyper", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" -dependencies = [ - "equivalent", - "hashbrown 0.16.0", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array 0.14.9", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lettre" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" -dependencies = [ - "async-trait", - "base64 0.22.1", - "chumsky", - "email-encoding", - "email_address", - "fastrand", - "futures-io", - "futures-util", - "hostname", - "httpdate", - "idna", - "mime", - "nom 8.0.0", - "percent-encoding", - "quoted_printable", - "rustls", - "socket2", - "tokio", - "tokio-rustls", - "url", - "webpki-roots", -] - -[[package]] -name = "libc" -version = "0.2.177" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[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 = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[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 = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes 1.10.1", - "encoding_rs", - "futures-util", - "http", - "httparse", - "memchr", - "mime", - "spin", - "tokio", - "version_check", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - -[[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-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.32.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "ordered-multimap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" -dependencies = [ - "dlv-list", - "hashbrown 0.14.5", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "pest_meta" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "phundrak-dot-com-backend" -version = "0.1.0" -dependencies = [ - "chrono", - "config", - "dotenvy", - "governor", - "lettre", - "poem", - "poem-openapi", - "serde", - "serde_json", - "thiserror", - "tokio", - "tracing", - "tracing-subscriber", - "validator", -] - -[[package]] -name = "pin-project" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef0f924a5ee7ea9cbcea77529dba45f8a9ba9f622419fe3386ca581a3ae9d5a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851c8d0ce9bebe43790dedfc86614c23494ac9f423dd618d3a61fc693eafe61e" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[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 = "poem" -version = "3.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f977080932c87287147dca052951c3e2696f8759863f6b4e4c0c9ffe7a4cc8b" -dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", - "chrono", - "cookie", - "csrf", - "futures-util", - "headers", - "http", - "http-body-util", - "hyper", - "hyper-util", - "mime", - "multer", - "nix", - "parking_lot", - "percent-encoding", - "pin-project-lite", - "poem-derive", - "quick-xml", - "regex", - "rfc7239", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "serde_yaml", - "smallvec", - "sse-codec", - "sync_wrapper", - "tempfile", - "thiserror", - "time", - "tokio", - "tokio-rustls", - "tokio-stream", - "tokio-util", - "tracing", - "wildmatch", -] - -[[package]] -name = "poem-derive" -version = "3.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "056e2fea6de1cb240ffe23cfc4fc370b629f8be83b5f27e16b7acd5231a72de4" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "poem-openapi" -version = "5.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ccbcc395bf4dd03df1da32da351b6b6732e4074ce27ddec315650e52a2be44c" -dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", - "chrono", - "derive_more", - "futures-util", - "indexmap", - "itertools 0.14.0", - "mime", - "num-traits", - "poem", - "poem-openapi-derive", - "quick-xml", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "serde_yaml", - "thiserror", - "tokio", -] - -[[package]] -name = "poem-openapi-derive" -version = "5.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41273b691a3d467a8c44d05506afba9f7b6bd56c9cdf80123de13fe52d7ec587" -dependencies = [ - "darling", - "http", - "indexmap", - "mime", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "syn 2.0.108", - "thiserror", -] - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.108", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[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 = "psm" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" -dependencies = [ - "ar_archive_writer", - "cc", -] - -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi", - "web-sys", - "winapi", -] - -[[package]] -name = "quick-xml" -version = "0.36.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "quote" -version = "1.0.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "quoted_printable" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "rfc7239" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a82f1d1e38e9a85bb58ffcfadf22ed6f2c94e8cd8581ec2b0f80a2a6858350f" -dependencies = [ - "uncased", -] - -[[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.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "ron" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" -dependencies = [ - "base64 0.21.7", - "bitflags", - "serde", - "serde_derive", -] - -[[package]] -name = "rust-ini" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" -dependencies = [ - "aws-lc-rs", - "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 = "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-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[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 2.0.108", -] - -[[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_spanned" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" -dependencies = [ - "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 = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "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 = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.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" - -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", -] - -[[package]] -name = "sse-codec" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a59f811350c44b4a037aabeb72dc6a9591fc22aa95a036db9a96297c58085a" -dependencies = [ - "bytes 0.5.6", - "futures-io", - "futures_codec", - "memchr", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stacker" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" -dependencies = [ - "bytes 1.10.1", - "libc", - "mio", - "pin-project-lite", - "socket2", - "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 2.0.108", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" -dependencies = [ - "bytes 1.10.1", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" -dependencies = [ - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" -dependencies = [ - "winnow", -] - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", -] - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "validator" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" -dependencies = [ - "idna", - "once_cell", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive", -] - -[[package]] -name = "validator_derive" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" -dependencies = [ - "darling", - "once_cell", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.108", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.105" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-roots" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "wildmatch" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b7d07a236abaef6607536ccfaf19b396dbe3f5110ddb73d39f4562902ed382" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.1", - "windows-result", - "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 2.0.108", -] - -[[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 2.0.108", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yaml-rust2" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] diff --git a/backend/Cargo.toml b/backend/Cargo.toml deleted file mode 100644 index 2f9e463..0000000 --- a/backend/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "phundrak-dot-com-backend" -version = "0.1.0" -edition = "2024" -publish = false -authors = ["Lucien Cartier-Tilet "] -license = "AGPL-3.0-only" - -[lib] -path = "src/lib.rs" - -[[bin]] -path = "src/main.rs" -name = "phundrak-dot-com-backend" - -[dependencies] -chrono = { version = "0.4.42", features = ["serde"] } -config = { version = "0.15.18", features = ["yaml"] } -dotenvy = "0.15.7" -governor = "0.8.0" -lettre = { version = "0.11.19", default-features = false, features = ["builder", "hostname", "pool", "rustls-tls", "tokio1", "tokio1-rustls-tls", "smtp-transport"] } -poem = { version = "3.1.12", default-features = false, features = ["csrf", "rustls", "test"] } -poem-openapi = { version = "5.1.16", features = ["chrono", "swagger-ui"] } -serde = "1.0.228" -serde_json = "1.0.145" -thiserror = "2.0.17" -tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] } -tracing = "0.1.41" -tracing-subscriber = { version = "0.3.20", features = ["fmt", "std", "env-filter", "registry", "json", "tracing-log"] } -validator = { version = "0.20.0", features = ["derive"] } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 87c34e4..0000000 --- a/backend/README.md +++ /dev/null @@ -1,424 +0,0 @@ -# phundrak.com Backend - -The backend for [phundrak.com](https://phundrak.com), built with Rust and the [Poem](https://github.com/poem-web/poem) web framework. - -## Features - -- **RESTful API** with automatic OpenAPI/Swagger documentation -- **Rate limiting** with configurable per-second limits using the - Generic Cell Rate Algorithm (thanks to - [`governor`](https://github.com/boinkor-net/governor)) -- **Contact form** with SMTP email relay (supports TLS, STARTTLS, and - unencrypted) -- **Type-safe routing** using Poem's declarative API -- **Hierarchical configuration** with YAML files and environment - variable overrides -- **Structured logging** with `tracing` and `tracing-subscriber` -- **Strict linting** for code quality and safety -- **Comprehensive testing** with integration test support - -## API Endpoints - -The application provides the following endpoints: - -- **Swagger UI**: `/` - Interactive API documentation -- **OpenAPI Spec**: `/specs` - OpenAPI specification in YAML format -- **Health Check**: `GET /api/health` - Returns server health status -- **Application Metadata**: `GET /api/meta` - Returns version and build info -- **Contact Form**: `POST /api/contact` - Submit contact form (relays to SMTP) - -## Configuration - -Configuration is loaded from multiple sources in order of precedence: - -1. `settings/base.yaml` - Base configuration -2. `settings/{environment}.yaml` - Environment-specific (development/production) -3. Environment variables prefixed with `APP__` (e.g., `APP__APPLICATION__PORT=8080`) - -The environment is determined by the `APP_ENVIRONMENT` variable (defaults to "development"). - -### Configuration Example - -```yaml -application: - port: 3100 - version: "0.1.0" - -email: - host: smtp.example.com - port: 587 - user: user@example.com - from: Contact Form - password: your_password - recipient: Admin - starttls: true # Use STARTTLS (typically port 587) - tls: false # Use implicit TLS (typically port 465) - -rate_limit: - enabled: true # Enable/disable rate limiting - burst_size: 10 # Maximum requests allowed in time window - per_seconds: 60 # Time window in seconds (100 req/60s = ~1.67 req/s) -``` - -You can also use a `.env` file for local development settings. - -### Rate Limiting - -The application includes built-in rate limiting to protect against abuse: - -- Uses the **Generic Cell Rate Algorithm (GCRA)** via the `governor` crate -- **In-memory rate limiting** - no external dependencies like Redis required -- **Configurable limits** via YAML configuration or environment variables -- **Per-second rate limiting** with burst support -- Returns `429 Too Many Requests` when limits are exceeded - -Default configuration: 100 requests per 60 seconds (approximately 1.67 requests per second with burst capacity). - -To disable rate limiting, set `rate_limit.enabled: false` in your configuration. - -## Development - -### Prerequisites - -**Option 1: Native Development** -- Rust (latest stable version recommended) -- Cargo (comes with Rust) - -**Option 2: Nix Development (Recommended)** -- [Nix](https://nixos.org/download) with flakes enabled -- All dependencies managed automatically - -### Running the Server - -**With Cargo:** -```bash -cargo run -``` - -**With Nix development shell:** -```bash -nix develop .#backend -cargo run -``` - -The server will start on the configured port (default: 3100). - -### Building - -**With Cargo:** - -For development builds: -```bash -cargo build -``` - -For optimized production builds: -```bash -cargo build --release -``` - -The compiled binary will be at `target/release/backend`. - -**With Nix:** - -Build the backend binary: -```bash -nix build .#backend -# Binary available at: ./result/bin/backend -``` - -Build Docker images: -```bash -# Build versioned Docker image (e.g., 0.1.0) -nix build .#backendDocker - -# Build latest Docker image -nix build .#backendDockerLatest - -# Load into Docker -docker load < result -# Image will be available as: localhost/phundrak/backend-rust:latest -``` - -The Nix build ensures reproducible builds with all dependencies pinned. - -## Testing - -Run all tests: - -```bash -cargo test -# or -just test -``` - -Run a specific test: - -```bash -cargo test -``` - -Run tests with output: - -```bash -cargo test -- --nocapture -``` - -Run tests with coverage: - -```bash -cargo tarpaulin --config .tarpaulin.local.toml -# or -just coverage -``` - -### Testing Notes - -- Integration tests use random TCP ports to avoid conflicts -- Tests use `get_test_app()` helper for consistent test setup -- Telemetry is automatically disabled during tests -- Tests are organized in `#[cfg(test)]` modules within each file - -## Code Quality - -### Linting - -This project uses extremely strict Clippy linting rules: - -- `#![deny(clippy::all)]` -- `#![deny(clippy::pedantic)]` -- `#![deny(clippy::nursery)]` -- `#![warn(missing_docs)]` - -Run Clippy to check for issues: - -```bash -cargo clippy --all-targets -# or -just lint -``` - -All code must pass these checks before committing. - -### Continuous Checking with Bacon - -For continuous testing and linting during development, use [bacon](https://dystroy.org/bacon/): - -```bash -bacon # Runs clippy-all by default -bacon test # Runs tests continuously -bacon clippy # Runs clippy on default target only -``` - -Press 'c' in bacon to run clippy-all. - -## Code Style - -### Error Handling - -- Use `thiserror` for custom error types -- Always return `Result` types for fallible operations -- Use descriptive error messages - -### Logging - -Always use `tracing::event!` with proper target and level: - -```rust -tracing::event!( - target: "backend", // or "backend::module_name" - tracing::Level::INFO, - "Message here" -); -``` - -### Imports - -Organize imports in three groups: -1. Standard library (`std::*`) -2. External crates (poem, serde, etc.) -3. Local modules (`crate::*`) - -### Testing Conventions - -- Use `#[tokio::test]` for async tests -- Use descriptive test names that explain what is being tested -- Test both success and error cases -- For endpoint tests, verify both status codes and response bodies - -## Project Structure - -``` -backend/ -β”œβ”€β”€ src/ -β”‚ β”œβ”€β”€ main.rs # Application entry point -β”‚ β”œβ”€β”€ lib.rs # Library root with run() and prepare() -β”‚ β”œβ”€β”€ startup.rs # Application builder, server setup -β”‚ β”œβ”€β”€ settings.rs # Configuration management -β”‚ β”œβ”€β”€ telemetry.rs # Logging and tracing setup -β”‚ β”œβ”€β”€ middleware/ # Custom middleware -β”‚ β”‚ β”œβ”€β”€ mod.rs # Middleware module -β”‚ β”‚ └── rate_limit.rs # Rate limiting middleware -β”‚ └── route/ # API route handlers -β”‚ β”œβ”€β”€ mod.rs # Route organization -β”‚ β”œβ”€β”€ contact.rs # Contact form endpoint -β”‚ β”œβ”€β”€ health.rs # Health check endpoint -β”‚ └── meta.rs # Metadata endpoint -β”œβ”€β”€ settings/ # Configuration files -β”‚ β”œβ”€β”€ base.yaml # Base configuration -β”‚ β”œβ”€β”€ development.yaml # Development overrides -β”‚ └── production.yaml # Production overrides -β”œβ”€β”€ Cargo.toml # Dependencies and metadata -└── README.md # This file -``` - -## Architecture - -### Application Initialization Flow - -1. `main.rs` calls `run()` from `lib.rs` -2. `run()` calls `prepare()` which: - - Loads environment variables from `.env` file - - Initializes `Settings` from YAML files and environment variables - - Sets up telemetry/logging (unless in test mode) - - Builds the `Application` with optional TCP listener -3. `Application::build()`: - - Sets up OpenAPI service with all API endpoints - - Configures Swagger UI at the root path (`/`) - - Configures API routes under `/api` prefix - - Creates server with TCP listener -4. Application runs with CORS middleware and settings injected as data - -### Email Handling - -The contact form supports multiple SMTP configurations: -- **Implicit TLS (SMTPS)** - typically port 465 -- **STARTTLS (Always/Opportunistic)** - typically port 587 -- **Unencrypted** (for local dev) - with or without authentication - -The `SmtpTransport` is built dynamically from `EmailSettings` based on -TLS/STARTTLS configuration. - -## Docker Deployment - -### Using Pre-built Images - -Docker images are automatically built and published via GitHub Actions to the configured container registry. - -Pull and run the latest image: -```bash -# Pull from Phundrak Labs (labs.phundrak.com) -docker pull labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest - -# Run the container -docker run -d \ - --name phundrak-backend \ - -p 3100:3100 \ - -e APP__APPLICATION__PORT=3100 \ - -e APP__EMAIL__HOST=smtp.example.com \ - -e APP__EMAIL__PORT=587 \ - -e APP__EMAIL__USER=user@example.com \ - -e APP__EMAIL__PASSWORD=your_password \ - -e APP__EMAIL__FROM="Contact Form " \ - -e APP__EMAIL__RECIPIENT="Admin " \ - labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest -``` - -### Available Image Tags - -The following tags are automatically published: - -- `latest` - Latest stable release (from tagged commits on `main`) -- `` - Specific version (e.g., `1.0.0`, from tagged commits like `v1.0.0`) -- `develop` - Latest development build (from `develop` branch) -- `pr` - Pull request preview builds (e.g., `pr12`) - -### Building Images Locally - -Build with Nix (recommended for reproducibility): -```bash -nix build .#backendDockerLatest -docker load < result -docker run -p 3100:3100 localhost/phundrak/backend-rust:latest -``` - -Build with Docker directly: -```bash -# Note: This requires a Dockerfile (not included in this project) -# Use Nix builds for containerization -``` - -### Docker Compose Example - -```yaml -version: '3.8' - -services: - backend: - image: labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest - ports: - - "3100:3100" - environment: - APP__APPLICATION__PORT: 3100 - APP__EMAIL__HOST: smtp.example.com - APP__EMAIL__PORT: 587 - APP__EMAIL__USER: ${SMTP_USER} - APP__EMAIL__PASSWORD: ${SMTP_PASSWORD} - APP__EMAIL__FROM: "Contact Form " - APP__EMAIL__RECIPIENT: "Admin " - APP__EMAIL__STARTTLS: true - APP__RATE_LIMIT__ENABLED: true - APP__RATE_LIMIT__BURST_SIZE: 10 - APP__RATE_LIMIT__PER_SECONDS: 60 - restart: unless-stopped -``` - -## CI/CD Pipeline - -### Automated Docker Publishing - -GitHub Actions automatically builds and publishes Docker images based on repository events: - -| Event Type | Trigger | Published Tags | -|-----------------|------------------------------|-------------------------------| -| Tag push | `v*.*.*` tag on `main` | `latest`, `` | -| Branch push | Push to `develop` | `develop` | -| Pull request | PR opened/updated | `pr` | -| Branch push | Push to `main` (no tag) | `latest` | - -### Workflow Details - -The CI/CD pipeline (`.github/workflows/publish-docker.yml`): - -1. **Checks out the repository** -2. **Installs Nix** with flakes enabled -3. **Builds the Docker image** using Nix for reproducibility -4. **Authenticates** with the configured Docker registry -5. **Tags and pushes** images based on the event type - -### Registry Configuration - -Images are published to the registry specified by the `DOCKER_REGISTRY` environment variable in the workflow (default: `labs.phundrak.com`). - -To use the published images, authenticate with the registry: - -```bash -# For Phundrak Labs (labs.phundrak.com) -echo $GITHUB_TOKEN | docker login labs.phundrak.com -u USERNAME --password-stdin - -# Pull the image -docker pull labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest -``` - -### Required Secrets - -The workflow requires these GitHub secrets: -- `DOCKER_USERNAME` - Registry username -- `DOCKER_PASSWORD` - Registry password or token -- `CACHIX_AUTH_TOKEN` - (Optional) For Nix build caching - -See [.github/workflows/README.md](../.github/workflows/README.md) for detailed setup instructions. - -## License - -AGPL-3.0-only - See the root repository for full license information. diff --git a/backend/bacon.toml b/backend/bacon.toml deleted file mode 100644 index 31c1d7a..0000000 --- a/backend/bacon.toml +++ /dev/null @@ -1,84 +0,0 @@ -# This is a configuration file for the bacon tool -# -# Bacon repository: https://github.com/Canop/bacon -# Complete help on configuration: https://dystroy.org/bacon/config/ -# You can also check bacon's own bacon.toml file -# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml - -default_job = "clippy-all" - -[jobs.check] -command = ["cargo", "check", "--color", "always"] -need_stdout = false - -[jobs.check-all] -command = ["cargo", "check", "--all-targets", "--color", "always"] -need_stdout = false - -# Run clippy on the default target -[jobs.clippy] -command = [ - "cargo", "clippy", - "--color", "always", -] -need_stdout = false - -[jobs.clippy-all] -command = [ - "cargo", "clippy", - "--all-targets", - "--color", "always", -] -need_stdout = false - -[jobs.test] -command = [ - "cargo", "test", "--color", "always", - "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 -] -need_stdout = true - -[jobs.doc] -command = ["cargo", "doc", "--color", "always", "--no-deps"] -need_stdout = false - -# If the doc compiles, then it opens in your browser and bacon switches -# to the previous job -[jobs.doc-open] -command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] -need_stdout = false -on_success = "back" # so that we don't open the browser at each change - -# You can run your application and have the result displayed in bacon, -# *if* it makes sense for this crate. -# Don't forget the `--color always` part or the errors won't be -# properly parsed. -# If your program never stops (eg a server), you may set `background` -# to false to have the cargo run output immediately displayed instead -# of waiting for program's end. -[jobs.run] -command = [ - "cargo", "run", - "--color", "always", - # put launch parameters for your program behind a `--` separator -] -need_stdout = true -allow_warnings = true -background = true - -# This parameterized job runs the example of your choice, as soon -# as the code compiles. -# Call it as -# bacon ex -- my-example -[jobs.ex] -command = ["cargo", "run", "--color", "always", "--example"] -need_stdout = true -allow_warnings = true - -# You may define here keybindings that would be specific to -# a project, for example a shortcut to launch a specific job. -# Shortcuts to internal functions (scrolling, toggling, etc.) -# should go in your personal global prefs.toml file instead. -[keybindings] -# alt-m = "job:my-job" -c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target diff --git a/backend/deny.toml b/backend/deny.toml deleted file mode 100644 index 67cba2d..0000000 --- a/backend/deny.toml +++ /dev/null @@ -1,51 +0,0 @@ -[output] -feature-depth = 1 - -[advisories] -ignore = [] - -[licenses] -# List of explicitly allowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -allow = [ - "0BSD", - "AGPL-3.0-only", - "Apache-2.0 WITH LLVM-exception", - "Apache-2.0", - "BSD-3-Clause", - "CDLA-Permissive-2.0", - "ISC", - "MIT", - "MPL-2.0", - "OpenSSL", - "Unicode-3.0", - "Zlib", -] -confidence-threshold = 0.8 -exceptions = [] - -[licenses.private] -ignore = false -registries = [] - -[bans] -multiple-versions = "allow" -wildcards = "allow" -highlight = "all" -workspace-default-features = "allow" -external-default-features = "allow" -allow = [] -deny = [] -skip = [] -skip-tree = [] - -[sources] -unknown-registry = "deny" -unknown-git = "deny" -allow-registry = ["https://github.com/rust-lang/crates.io-index"] -allow-git = [] - -[sources.allow-org] -github = [] -gitlab = [] -bitbucket = [] diff --git a/backend/justfile b/backend/justfile deleted file mode 100644 index bd8df91..0000000 --- a/backend/justfile +++ /dev/null @@ -1,48 +0,0 @@ -default: run - -run: - cargo run - -run-release: - cargo run --release - -format: - cargo fmt --all - -format-check: - cargo fmt --check --all - -audit: - cargo deny - -build: - cargo build - -build-release: - cargo build --release - -lint: - cargo clippy --all-targets - -release-build: - cargo build --release - -release-run: - cargo run --release - -test: - cargo test - -coverage: - mkdir -p coverage - cargo tarpaulin --config .tarpaulin.local.toml - -coverage-ci: - mkdir -p coverage - cargo tarpaulin --config .tarpaulin.ci.toml - -check-all: format-check lint coverage audit - -## Local Variables: -## mode: makefile -## End: diff --git a/backend/nix/package.nix b/backend/nix/package.nix deleted file mode 100644 index 1d12f7a..0000000 --- a/backend/nix/package.nix +++ /dev/null @@ -1,60 +0,0 @@ -{ - rust-overlay, - inputs, - system, - ... -}: let - rust = import ./rust-version.nix { inherit rust-overlay inputs system; }; - pkgs = rust.pkgs; - rustPlatform = pkgs.makeRustPlatform { - cargo = rust.version; - rustc = rust.version; - }; - cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml); - name = cargoToml.package.name; - version = cargoToml.package.version; - rustBuild = rustPlatform.buildRustPackage { - pname = name; - inherit version; - src = ../.; - cargoLock.lockFile = ../Cargo.lock; - }; - settingsDir = pkgs.runCommand "settings" {} '' - mkdir -p $out/settings - cp ${../settings}/*.yaml $out/settings/ - ''; - makeDockerImage = tag: - pkgs.dockerTools.buildLayeredImage { - name = "phundrak/${name}"; - inherit tag; - created = "now"; - config = { - Entrypoint = ["${rustBuild}/bin/${name}"]; - WorkingDir = "/"; - Env = [ - "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" - ]; - ExposedPorts = { - "3100/tcp" = {}; - }; - Labels = { - "org.opencontainers.image.title" = name; - "org.opencontainers.image.version" = version; - "org.opencontainers.image.description" = "REST API backend for phundrak.com"; - "org.opencontainers.image.authors" = "Lucien Cartier-Tilet "; - "org.opencontainers.image.licenses" = "AGPL-3.0-only"; - "org.opencontainers.image.source" = "https://labs.phundrak.com/phundrak/phundrak.com"; - "org.opencontainers.image.url" = "https://labs.phundrak.com/phundrak/phundrak.com"; - "org.opencontainers.image.documentation" = "https://labs.phundrak.com/phundrak/phundrak.com"; - "org.opencontainers.image.vendor" = "Phundrak"; - }; - }; - contents = [rustBuild pkgs.cacert settingsDir]; - }; - dockerImageLatest = makeDockerImage "latest"; - dockerImageVersioned = makeDockerImage version; -in { - backend = rustBuild; - backendDocker = dockerImageVersioned; - backendDockerLatest = dockerImageLatest; -} diff --git a/backend/nix/rust-version.nix b/backend/nix/rust-version.nix deleted file mode 100644 index 07e63ae..0000000 --- a/backend/nix/rust-version.nix +++ /dev/null @@ -1,6 +0,0 @@ -{rust-overlay, inputs, system, ...}: let -overlays = [(import rust-overlay)]; -in rec { - pkgs = import inputs.nixpkgs {inherit system overlays;}; - version = pkgs.rust-bin.stable.latest.default; -} diff --git a/backend/nix/shell.nix b/backend/nix/shell.nix deleted file mode 100644 index 6c8fc58..0000000 --- a/backend/nix/shell.nix +++ /dev/null @@ -1,75 +0,0 @@ -{ - inputs, - pkgs, - system, - self, - rust-overlay, - ... -}: let - rustPlatform = import ./rust-version.nix { inherit rust-overlay inputs system; }; -in - inputs.devenv.lib.mkShell { - inherit inputs pkgs; - modules = [ - { - devenv.root = let - devenvRootFileContent = builtins.readFile "${self}/.devenv-root"; - in - pkgs.lib.mkIf (devenvRootFileContent != "") devenvRootFileContent; - } - { - packages = with rustPlatform.pkgs; [ - (rustPlatform.version.override { - extensions = [ - "clippy" - "rust-src" - "rust-analyzer" - "rustfmt" - ]; - }) - bacon - cargo-deny - cargo-shuttle - cargo-tarpaulin - cargo-watch - flyctl - just - marksman - tombi # TOML lsp server - ]; - - services.mailpit = { - enable = true; - # HTTP interface for viewing emails - uiListenAddress = "127.0.0.1:8025"; - # SMTP server for receiving emails - smtpListenAddress = "127.0.0.1:1025"; - }; - - processes.run.exec = "cargo watch -x run"; - - enterShell = '' - echo "πŸ¦€ Rust backend development environment loaded!" - echo "πŸ“¦ Rust version: $(rustc --version)" - echo "πŸ“¦ Cargo version: $(cargo --version)" - echo "" - echo "Available tools:" - echo " - rust-analyzer (LSP)" - echo " - clippy (linter)" - echo " - rustfmt (formatter)" - echo " - bacon (continuous testing/linting)" - echo " - cargo-deny (dependency checker)" - echo " - cargo-tarpaulin (code coverage)" - echo "" - echo "πŸ“§ Mailpit service:" - echo " - SMTP server: 127.0.0.1:1025" - echo " - Web UI: http://127.0.0.1:8025" - echo "" - echo "πŸš€ Quick start:" - echo " Run 'devenv up' to launch:" - echo " - Mailpit service (email testing)" - echo " - Backend with 'cargo watch -x run' (auto-reload)" - ''; - } - ]; - } diff --git a/backend/settings/base.yaml b/backend/settings/base.yaml deleted file mode 100644 index 4b7f6fa..0000000 --- a/backend/settings/base.yaml +++ /dev/null @@ -1,8 +0,0 @@ -application: - port: 3100 - version: "0.1.0" - -rate_limit: - enabled: true - burst_size: 10 - per_seconds: 60 diff --git a/backend/settings/development.yaml b/backend/settings/development.yaml deleted file mode 100644 index ae60e88..0000000 --- a/backend/settings/development.yaml +++ /dev/null @@ -1,18 +0,0 @@ -frontend_url: http://localhost:3000 -debug: true - -application: - protocol: http - host: 127.0.0.1 - base_url: http://127.0.0.1:3100 - name: "com.phundrak.backend.dev" - -email: - host: localhost - port: 1025 - user: "" - password: "" - from: Contact Form - recipient: Admin - tls: false - starttls: false diff --git a/backend/settings/production.yaml b/backend/settings/production.yaml deleted file mode 100644 index d5bec1c..0000000 --- a/backend/settings/production.yaml +++ /dev/null @@ -1,18 +0,0 @@ -debug: false -frontend_url: "" - -application: - name: "com.phundrak.backend.prod" - protocol: https - host: 0.0.0.0 - base_url: "" - -email: - host: "" - port: 0 - user: "" - password: "" - from: "" - recipient: "" - tls: false - starttls: false diff --git a/backend/src/lib.rs b/backend/src/lib.rs deleted file mode 100644 index 16662b7..0000000 --- a/backend/src/lib.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! Backend API server for phundrak.com -//! -//! This is a REST API built with the Poem framework that provides: -//! - Health check endpoints -//! - Application metadata endpoints -//! - Contact form submission with email integration - -#![deny(clippy::all)] -#![deny(clippy::pedantic)] -#![deny(clippy::nursery)] -#![warn(missing_docs)] -#![allow(clippy::unused_async)] - -/// Custom middleware implementations -pub mod middleware; -/// API route handlers and endpoints -pub mod route; -/// Application configuration settings -pub mod settings; -/// Application startup and server configuration -pub mod startup; -/// Logging and tracing setup -pub mod telemetry; - -type MaybeListener = Option>; - -fn prepare(listener: MaybeListener) -> startup::Application { - dotenvy::dotenv().ok(); - let settings = settings::Settings::new().expect("Failed to read settings"); - if !cfg!(test) { - let subscriber = telemetry::get_subscriber(settings.debug); - telemetry::init_subscriber(subscriber); - } - tracing::event!( - target: "backend", - tracing::Level::DEBUG, - "Using these settings: {:?}", - settings - ); - let application = startup::Application::build(settings, listener); - tracing::event!( - target: "backend", - tracing::Level::INFO, - "Listening on http://{}:{}/", - application.host(), - application.port() - ); - tracing::event!( - target: "backend", - tracing::Level::INFO, - "Documentation available at http://{}:{}/", - application.host(), - application.port() - ); - application -} - -/// Runs the application with the specified TCP listener. -/// -/// # Errors -/// -/// Returns a `std::io::Error` if the server fails to start or encounters -/// an I/O error during runtime (e.g., port already in use, network issues). -#[cfg(not(tarpaulin_include))] -pub async fn run(listener: MaybeListener) -> Result<(), std::io::Error> { - let application = prepare(listener); - application.make_app().run().await -} - -#[cfg(test)] -fn make_random_tcp_listener() -> poem::listener::TcpListener { - let tcp_listener = - std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind a random TCP listener"); - let port = tcp_listener.local_addr().unwrap().port(); - poem::listener::TcpListener::bind(format!("127.0.0.1:{port}")) -} - -#[cfg(test)] -fn get_test_app() -> startup::App { - let tcp_listener = make_random_tcp_listener(); - prepare(Some(tcp_listener)).make_app().into() -} diff --git a/backend/src/main.rs b/backend/src/main.rs deleted file mode 100644 index 0fda2a3..0000000 --- a/backend/src/main.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Backend server entry point. - -#[cfg(not(tarpaulin_include))] -#[tokio::main] -async fn main() -> Result<(), std::io::Error> { - phundrak_dot_com_backend::run(None).await -} diff --git a/backend/src/middleware/mod.rs b/backend/src/middleware/mod.rs deleted file mode 100644 index 16c713b..0000000 --- a/backend/src/middleware/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Custom middleware for the application. -//! -//! This module contains custom middleware implementations including rate limiting. - -pub mod rate_limit; diff --git a/backend/src/middleware/rate_limit.rs b/backend/src/middleware/rate_limit.rs deleted file mode 100644 index 42aee34..0000000 --- a/backend/src/middleware/rate_limit.rs +++ /dev/null @@ -1,211 +0,0 @@ -//! Rate limiting middleware using the governor crate. -//! -//! This middleware implements per-IP rate limiting using the Generic Cell Rate -//! Algorithm (GCRA) via the governor crate. It stores rate limiters in memory -//! without requiring external dependencies like Redis. - -use std::{ - net::IpAddr, - num::NonZeroU32, - sync::Arc, - time::Duration, -}; - -use governor::{ - clock::DefaultClock, - state::{InMemoryState, NotKeyed}, - Quota, RateLimiter, -}; -use poem::{ - Endpoint, Error, IntoResponse, Middleware, Request, Response, Result, -}; - -/// Rate limiting configuration. -#[derive(Debug, Clone)] -pub struct RateLimitConfig { - /// Maximum number of requests allowed in the time window (burst size). - pub burst_size: u32, - /// Time window in seconds for rate limiting. - pub per_seconds: u64, -} - -impl RateLimitConfig { - /// Creates a new rate limit configuration. - /// - /// # Arguments - /// - /// * `burst_size` - Maximum number of requests allowed in the time window - /// * `per_seconds` - Time window in seconds - #[must_use] - pub const fn new(burst_size: u32, per_seconds: u64) -> Self { - Self { - burst_size, - per_seconds, - } - } - - /// Creates a rate limiter from this configuration. - /// - /// # Panics - /// - /// Panics if `burst_size` is zero. - #[must_use] - pub fn create_limiter(&self) -> RateLimiter { - let quota = Quota::with_period(Duration::from_secs(self.per_seconds)) - .expect("Failed to create quota") - .allow_burst(NonZeroU32::new(self.burst_size).expect("Burst size must be non-zero")); - RateLimiter::direct(quota) - } -} - -impl Default for RateLimitConfig { - fn default() -> Self { - // Default: 10 requests per second with burst of 20 - Self::new(20, 1) - } -} - -/// Middleware for rate limiting based on IP address. -pub struct RateLimit { - limiter: Arc>, -} - -impl RateLimit { - /// Creates a new rate limiting middleware with the given configuration. - #[must_use] - pub fn new(config: &RateLimitConfig) -> Self { - Self { - limiter: Arc::new(config.create_limiter()), - } - } -} - -impl Middleware for RateLimit { - type Output = RateLimitEndpoint; - - fn transform(&self, ep: E) -> Self::Output { - RateLimitEndpoint { - endpoint: ep, - limiter: self.limiter.clone(), - } - } -} - -/// The endpoint wrapper that performs rate limiting checks. -pub struct RateLimitEndpoint { - endpoint: E, - limiter: Arc>, -} - -impl Endpoint for RateLimitEndpoint { - type Output = Response; - - async fn call(&self, req: Request) -> Result { - // Check rate limit - if self.limiter.check().is_err() { - let client_ip = Self::get_client_ip(&req) - .map_or_else(|| "unknown".to_string(), |ip| ip.to_string()); - - tracing::event!( - target: "backend::middleware::rate_limit", - tracing::Level::WARN, - client_ip = %client_ip, - "Rate limit exceeded" - ); - - return Err(Error::from_status(poem::http::StatusCode::TOO_MANY_REQUESTS)); - } - - // Process the request - let response = self.endpoint.call(req).await; - response.map(IntoResponse::into_response) - } -} - -impl RateLimitEndpoint { - /// Extracts the client IP address from the request. - fn get_client_ip(req: &Request) -> Option { - req.remote_addr().as_socket_addr().map(std::net::SocketAddr::ip) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rate_limit_config_new() { - let config = RateLimitConfig::new(10, 60); - assert_eq!(config.burst_size, 10); - assert_eq!(config.per_seconds, 60); - } - - #[test] - fn rate_limit_config_default() { - let config = RateLimitConfig::default(); - assert_eq!(config.burst_size, 20); - assert_eq!(config.per_seconds, 1); - } - - #[test] - fn rate_limit_config_creates_limiter() { - let config = RateLimitConfig::new(5, 1); - let limiter = config.create_limiter(); - - // First 5 requests should succeed - for _ in 0..5 { - assert!(limiter.check().is_ok()); - } - - // 6th request should fail - assert!(limiter.check().is_err()); - } - - #[tokio::test] - async fn rate_limit_middleware_allows_within_limit() { - use poem::{handler, test::TestClient, EndpointExt, Route}; - - #[handler] - async fn index() -> String { - "Hello".to_string() - } - - let config = RateLimitConfig::new(5, 60); - let app = Route::new() - .at("/", poem::get(index)) - .with(RateLimit::new(&config)); - let cli = TestClient::new(app); - - // First 5 requests should succeed - for _ in 0..5 { - let response = cli.get("/").send().await; - response.assert_status_is_ok(); - } - } - - #[tokio::test] - async fn rate_limit_middleware_blocks_over_limit() { - use poem::{handler, test::TestClient, EndpointExt, Route}; - - #[handler] - async fn index() -> String { - "Hello".to_string() - } - - let config = RateLimitConfig::new(3, 60); - let app = Route::new() - .at("/", poem::get(index)) - .with(RateLimit::new(&config)); - let cli = TestClient::new(app); - - // First 3 requests should succeed - for _ in 0..3 { - let response = cli.get("/").send().await; - response.assert_status_is_ok(); - } - - // 4th request should be rate limited - let response = cli.get("/").send().await; - response.assert_status(poem::http::StatusCode::TOO_MANY_REQUESTS); - } -} diff --git a/backend/src/route/contact.rs b/backend/src/route/contact.rs deleted file mode 100644 index 9b082ec..0000000 --- a/backend/src/route/contact.rs +++ /dev/null @@ -1,514 +0,0 @@ -//! Contact form endpoint for handling user submissions and sending emails. -//! -//! This module provides functionality to: -//! - Validate contact form submissions -//! - Detect spam using honeypot fields -//! - Send emails via SMTP with various TLS configurations - -use lettre::{ - Message, SmtpTransport, Transport, message::header::ContentType, - transport::smtp::authentication::Credentials, -}; -use poem_openapi::{ApiResponse, Object, OpenApi, payload::Json}; -use validator::Validate; - -use super::ApiCategory; -use crate::settings::{EmailSettings, Starttls}; - -impl TryFrom<&EmailSettings> for SmtpTransport { - type Error = lettre::transport::smtp::Error; - - fn try_from(settings: &EmailSettings) -> Result { - if settings.tls { - // Implicit TLS (SMTPS) - typically port 465 - tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using implicit TLS (SMTPS)"); - let creds = Credentials::new(settings.user.clone(), settings.password.clone()); - Ok(Self::relay(&settings.host)? - .port(settings.port) - .credentials(creds) - .build()) - } else { - // STARTTLS or no encryption - match settings.starttls { - Starttls::Never => { - // For local development without TLS - tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using unencrypted connection"); - let builder = Self::builder_dangerous(&settings.host).port(settings.port); - if settings.user.is_empty() { - Ok(builder.build()) - } else { - let creds = - Credentials::new(settings.user.clone(), settings.password.clone()); - Ok(builder.credentials(creds).build()) - } - } - Starttls::Opportunistic | Starttls::Always => { - // STARTTLS - typically port 587 - tracing::event!(target: "backend::contact", tracing::Level::DEBUG, "Using STARTTLS"); - let creds = Credentials::new(settings.user.clone(), settings.password.clone()); - Ok(Self::starttls_relay(&settings.host)? - .port(settings.port) - .credentials(creds) - .build()) - } - } - } - } -} - -#[derive(Debug, Object, Validate)] -struct ContactRequest { - #[validate(length( - min = 1, - max = "100", - message = "Name must be between 1 and 100 characters" - ))] - name: String, - #[validate(email(message = "Invalid email address"))] - email: String, - #[validate(length( - min = 10, - max = 5000, - message = "Message must be between 10 and 5000 characters" - ))] - message: String, - /// Honeypot field - should always be empty - #[oai(rename = "website")] - honeypot: Option, -} - -#[derive(Debug, Object, serde::Deserialize)] -struct ContactResponse { - success: bool, - message: String, -} - -impl From for Json { - fn from(value: ContactResponse) -> Self { - Self(value) - } -} - -#[derive(ApiResponse)] -enum ContactApiResponse { - /// Success - #[oai(status = 200)] - Ok(Json), - /// Bad Request - validation failed - #[oai(status = 400)] - BadRequest(Json), - /// Too Many Requests - rate limit exceeded - #[oai(status = 429)] - #[allow(dead_code)] - TooManyRequests, - /// Internal Server Error - #[oai(status = 500)] - InternalServerError(Json), -} - -/// API for handling contact form submissions and sending emails. -#[derive(Clone)] -pub struct ContactApi { - settings: EmailSettings, -} - -impl From for ContactApi { - fn from(settings: EmailSettings) -> Self { - Self { settings } - } -} - -#[OpenApi(tag = "ApiCategory::Contact")] -impl ContactApi { - /// Submit a contact form - /// - /// Send a message through the contact form. Rate limited to prevent spam. - #[oai(path = "/contact", method = "post")] - async fn submit_contact( - &self, - body: Json, - remote_addr: Option>, - ) -> ContactApiResponse { - let body = body.0; - if body.honeypot.is_some() { - tracing::event!(target: "backend::contact", tracing::Level::INFO, "Honeypot triggered, rejecting request silently. IP: {}", remote_addr.map_or_else(|| "No remote address found".to_owned(), |ip| ip.0.to_string())); - return ContactApiResponse::Ok( - ContactResponse { - success: true, - message: "Message sent successfully, but not really, you bot".to_owned(), - } - .into(), - ); - } - if let Err(e) = body.validate() { - return ContactApiResponse::BadRequest( - ContactResponse { - success: false, - message: format!("Validation error: {e}"), - } - .into(), - ); - } - match self.send_email(&body).await { - Ok(()) => { - tracing::event!(target: "backend::contact", tracing::Level::INFO, "Message sent successfully from: {}", body.email); - ContactApiResponse::Ok( - ContactResponse { - success: true, - message: "Message sent successfully".to_owned(), - } - .into(), - ) - } - Err(e) => { - tracing::event!(target: "backend::contact", tracing::Level::ERROR, "Failed to send email: {}", e); - ContactApiResponse::InternalServerError( - ContactResponse { - success: false, - message: "Failed to send message. Please try again later.".to_owned(), - } - .into(), - ) - } - } - } - - async fn send_email(&self, request: &ContactRequest) -> Result<(), Box> { - let email_body = format!( - r"New contact form submission: - -Name: {} -Email: {}, - -Message: -{}", - request.name, request.email, request.message - ); - tracing::event!(target: "email", tracing::Level::DEBUG, "Sending email content: {}", email_body); - let email = Message::builder() - .from(self.settings.from.parse()?) - .reply_to(format!("{} <{}>", request.name, request.email).parse()?) - .to(self.settings.recipient.parse()?) - .subject(format!("Contact Form: {}", request.name)) - .header(ContentType::TEXT_PLAIN) - .body(email_body)?; - tracing::event!(target: "email", tracing::Level::DEBUG, "Email to be sent: {}", format!("{email:?}")); - - let mailer = SmtpTransport::try_from(&self.settings)?; - mailer.send(&email)?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Tests for ContactRequest validation - #[test] - fn contact_request_valid() { - let request = ContactRequest { - name: "John Doe".to_string(), - email: "john@example.com".to_string(), - message: "This is a test message that is long enough.".to_string(), - honeypot: None, - }; - assert!(request.validate().is_ok()); - } - - #[test] - fn contact_request_name_too_short() { - let request = ContactRequest { - name: String::new(), - email: "john@example.com".to_string(), - message: "This is a test message that is long enough.".to_string(), - honeypot: None, - }; - assert!(request.validate().is_err()); - } - - #[test] - fn contact_request_name_too_long() { - let request = ContactRequest { - name: "a".repeat(101), - email: "john@example.com".to_string(), - message: "This is a test message that is long enough.".to_string(), - honeypot: None, - }; - assert!(request.validate().is_err()); - } - - #[test] - fn contact_request_name_at_max_length() { - let request = ContactRequest { - name: "a".repeat(100), - email: "john@example.com".to_string(), - message: "This is a test message that is long enough.".to_string(), - honeypot: None, - }; - assert!(request.validate().is_ok()); - } - - #[test] - fn contact_request_invalid_email() { - let request = ContactRequest { - name: "John Doe".to_string(), - email: "not-an-email".to_string(), - message: "This is a test message that is long enough.".to_string(), - honeypot: None, - }; - assert!(request.validate().is_err()); - } - - #[test] - fn contact_request_message_too_short() { - let request = ContactRequest { - name: "John Doe".to_string(), - email: "john@example.com".to_string(), - message: "Short".to_string(), - honeypot: None, - }; - assert!(request.validate().is_err()); - } - - #[test] - fn contact_request_message_too_long() { - let request = ContactRequest { - name: "John Doe".to_string(), - email: "john@example.com".to_string(), - message: "a".repeat(5001), - honeypot: None, - }; - assert!(request.validate().is_err()); - } - - #[test] - fn contact_request_message_at_min_length() { - let request = ContactRequest { - name: "John Doe".to_string(), - email: "john@example.com".to_string(), - message: "a".repeat(10), - honeypot: None, - }; - assert!(request.validate().is_ok()); - } - - #[test] - fn contact_request_message_at_max_length() { - let request = ContactRequest { - name: "John Doe".to_string(), - email: "john@example.com".to_string(), - message: "a".repeat(5000), - honeypot: None, - }; - assert!(request.validate().is_ok()); - } - - // Tests for SmtpTransport TryFrom implementation - #[test] - fn smtp_transport_implicit_tls() { - let settings = EmailSettings { - host: "smtp.example.com".to_string(), - port: 465, - user: "user@example.com".to_string(), - password: "password".to_string(), - from: "from@example.com".to_string(), - recipient: "to@example.com".to_string(), - tls: true, - starttls: Starttls::Never, - }; - - let result = SmtpTransport::try_from(&settings); - assert!(result.is_ok()); - } - - #[test] - fn smtp_transport_starttls_always() { - let settings = EmailSettings { - host: "smtp.example.com".to_string(), - port: 587, - user: "user@example.com".to_string(), - password: "password".to_string(), - from: "from@example.com".to_string(), - recipient: "to@example.com".to_string(), - tls: false, - starttls: Starttls::Always, - }; - - let result = SmtpTransport::try_from(&settings); - assert!(result.is_ok()); - } - - #[test] - fn smtp_transport_starttls_opportunistic() { - let settings = EmailSettings { - host: "smtp.example.com".to_string(), - port: 587, - user: "user@example.com".to_string(), - password: "password".to_string(), - from: "from@example.com".to_string(), - recipient: "to@example.com".to_string(), - tls: false, - starttls: Starttls::Opportunistic, - }; - - let result = SmtpTransport::try_from(&settings); - assert!(result.is_ok()); - } - - #[test] - fn smtp_transport_no_encryption_with_credentials() { - let settings = EmailSettings { - host: "localhost".to_string(), - port: 1025, - user: "user@example.com".to_string(), - password: "password".to_string(), - from: "from@example.com".to_string(), - recipient: "to@example.com".to_string(), - tls: false, - starttls: Starttls::Never, - }; - - let result = SmtpTransport::try_from(&settings); - assert!(result.is_ok()); - } - - #[test] - fn smtp_transport_no_encryption_no_credentials() { - let settings = EmailSettings { - host: "localhost".to_string(), - port: 1025, - user: String::new(), - password: String::new(), - from: "from@example.com".to_string(), - recipient: "to@example.com".to_string(), - tls: false, - starttls: Starttls::Never, - }; - - let result = SmtpTransport::try_from(&settings); - assert!(result.is_ok()); - } - - // Integration tests for contact API endpoint - #[tokio::test] - async fn contact_endpoint_honeypot_triggered() { - let app = crate::get_test_app(); - let cli = poem::test::TestClient::new(app); - - let body = serde_json::json!({ - "name": "Bot Name", - "email": "bot@example.com", - "message": "This is a spam message from a bot.", - "website": "http://spam.com" - }); - - let resp = cli.post("/api/contact").body_json(&body).send().await; - resp.assert_status_is_ok(); - - let json_text = resp.0.into_body().into_string().await.unwrap(); - let json: ContactResponse = serde_json::from_str(&json_text).unwrap(); - assert!(json.success); - assert!(json.message.contains("not really")); - } - - #[tokio::test] - async fn contact_endpoint_validation_error_empty_name() { - let app = crate::get_test_app(); - let cli = poem::test::TestClient::new(app); - - let body = serde_json::json!({ - "name": "", - "email": "test@example.com", - "message": "This is a valid message that is long enough." - }); - - let resp = cli.post("/api/contact").body_json(&body).send().await; - resp.assert_status(poem::http::StatusCode::BAD_REQUEST); - - let json_text = resp.0.into_body().into_string().await.unwrap(); - let json: ContactResponse = serde_json::from_str(&json_text).unwrap(); - assert!(!json.success); - assert!(json.message.contains("Validation error")); - } - - #[tokio::test] - async fn contact_endpoint_validation_error_invalid_email() { - let app = crate::get_test_app(); - let cli = poem::test::TestClient::new(app); - - let body = serde_json::json!({ - "name": "Test User", - "email": "not-an-email", - "message": "This is a valid message that is long enough." - }); - - let resp = cli.post("/api/contact").body_json(&body).send().await; - resp.assert_status(poem::http::StatusCode::BAD_REQUEST); - - let json_text = resp.0.into_body().into_string().await.unwrap(); - let json: ContactResponse = serde_json::from_str(&json_text).unwrap(); - assert!(!json.success); - assert!(json.message.contains("Validation error")); - } - - #[tokio::test] - async fn contact_endpoint_validation_error_message_too_short() { - let app = crate::get_test_app(); - let cli = poem::test::TestClient::new(app); - - let body = serde_json::json!({ - "name": "Test User", - "email": "test@example.com", - "message": "Short" - }); - - let resp = cli.post("/api/contact").body_json(&body).send().await; - resp.assert_status(poem::http::StatusCode::BAD_REQUEST); - - let json_text = resp.0.into_body().into_string().await.unwrap(); - let json: ContactResponse = serde_json::from_str(&json_text).unwrap(); - assert!(!json.success); - assert!(json.message.contains("Validation error")); - } - - #[tokio::test] - async fn contact_endpoint_validation_error_name_too_long() { - let app = crate::get_test_app(); - let cli = poem::test::TestClient::new(app); - - let body = serde_json::json!({ - "name": "a".repeat(101), - "email": "test@example.com", - "message": "This is a valid message that is long enough." - }); - - let resp = cli.post("/api/contact").body_json(&body).send().await; - resp.assert_status(poem::http::StatusCode::BAD_REQUEST); - - let json_text = resp.0.into_body().into_string().await.unwrap(); - let json: ContactResponse = serde_json::from_str(&json_text).unwrap(); - assert!(!json.success); - assert!(json.message.contains("Validation error")); - } - - #[tokio::test] - async fn contact_endpoint_validation_error_message_too_long() { - let app = crate::get_test_app(); - let cli = poem::test::TestClient::new(app); - - let body = serde_json::json!({ - "name": "Test User", - "email": "test@example.com", - "message": "a".repeat(5001) - }); - - let resp = cli.post("/api/contact").body_json(&body).send().await; - resp.assert_status(poem::http::StatusCode::BAD_REQUEST); - - let json_text = resp.0.into_body().into_string().await.unwrap(); - let json: ContactResponse = serde_json::from_str(&json_text).unwrap(); - assert!(!json.success); - assert!(json.message.contains("Validation error")); - } -} diff --git a/backend/src/route/health.rs b/backend/src/route/health.rs deleted file mode 100644 index 0d6a4bb..0000000 --- a/backend/src/route/health.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Health check endpoint for monitoring service availability. - -use poem_openapi::{ApiResponse, OpenApi}; - -use super::ApiCategory; - -#[derive(ApiResponse)] -enum HealthResponse { - /// Success - #[oai(status = 200)] - Ok, - /// Too Many Requests - rate limit exceeded - #[oai(status = 429)] - #[allow(dead_code)] - TooManyRequests, -} - -/// Health check API for monitoring service availability. -#[derive(Default, Clone)] -pub struct HealthApi; - -#[OpenApi(tag = "ApiCategory::Health")] -impl HealthApi { - #[oai(path = "/health", method = "get")] - async fn ping(&self) -> HealthResponse { - tracing::event!(target: "backend::health", tracing::Level::DEBUG, "Accessing health-check endpoint"); - HealthResponse::Ok - } -} - -#[tokio::test] -async fn health_check_works() { - let app = crate::get_test_app(); - let cli = poem::test::TestClient::new(app); - let resp = cli.get("/api/health").send().await; - resp.assert_status_is_ok(); - resp.assert_text("").await; -} diff --git a/backend/src/route/meta.rs b/backend/src/route/meta.rs deleted file mode 100644 index c2083de..0000000 --- a/backend/src/route/meta.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Application metadata endpoint for retrieving version and name information. - -use poem::Result; -use poem_openapi::{ApiResponse, Object, OpenApi, payload::Json}; - -use super::ApiCategory; -use crate::settings::ApplicationSettings; - -#[derive(Object, Debug, Clone, serde::Serialize, serde::Deserialize)] -struct Meta { - version: String, - name: String, -} - -impl From<&MetaApi> for Meta { - fn from(value: &MetaApi) -> Self { - let version = value.version.clone(); - let name = value.name.clone(); - Self { version, name } - } -} - -#[derive(ApiResponse)] -enum MetaResponse { - /// Success - #[oai(status = 200)] - Meta(Json), - /// Too Many Requests - rate limit exceeded - #[oai(status = 429)] - #[allow(dead_code)] - TooManyRequests, -} - -/// API for retrieving application metadata (name and version). -#[derive(Clone)] -pub struct MetaApi { - name: String, - version: String, -} - -impl From<&ApplicationSettings> for MetaApi { - fn from(value: &ApplicationSettings) -> Self { - let name = value.name.clone(); - let version = value.version.clone(); - Self { name, version } - } -} - -#[OpenApi(tag = "ApiCategory::Meta")] -impl MetaApi { - #[oai(path = "/meta", method = "get")] - async fn meta(&self) -> Result { - tracing::event!(target: "backend::meta", tracing::Level::DEBUG, "Accessing meta endpoint"); - Ok(MetaResponse::Meta(Json(self.into()))) - } -} - -#[cfg(test)] -mod tests { - #[tokio::test] - async fn meta_endpoint_returns_correct_data() { - let app = crate::get_test_app(); - let cli = poem::test::TestClient::new(app); - let resp = cli.get("/api/meta").send().await; - resp.assert_status_is_ok(); - - let json_value: serde_json::Value = resp.json().await.value().deserialize(); - - assert!( - json_value.get("version").is_some(), - "Response should have version field" - ); - assert!( - json_value.get("name").is_some(), - "Response should have name field" - ); - } - - #[tokio::test] - async fn meta_endpoint_returns_200_status() { - let app = crate::get_test_app(); - let cli = poem::test::TestClient::new(app); - let resp = cli.get("/api/meta").send().await; - resp.assert_status_is_ok(); - } -} diff --git a/backend/src/route/mod.rs b/backend/src/route/mod.rs deleted file mode 100644 index 8801189..0000000 --- a/backend/src/route/mod.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! API route handlers for the backend server. -//! -//! This module contains all the HTTP endpoint handlers organized by functionality: -//! - Contact form handling -//! - Health checks -//! - Application metadata - -use poem_openapi::Tags; - -mod contact; -mod health; -mod meta; - -use crate::settings::Settings; - -#[derive(Tags)] -enum ApiCategory { - Contact, - Health, - Meta, -} - -pub(crate) struct Api { - contact: contact::ContactApi, - health: health::HealthApi, - meta: meta::MetaApi, -} - -impl From<&Settings> for Api { - fn from(value: &Settings) -> Self { - let contact = contact::ContactApi::from(value.clone().email); - let health = health::HealthApi; - let meta = meta::MetaApi::from(&value.application); - Self { - contact, - health, - meta, - } - } -} - -impl Api { - pub fn apis(self) -> (contact::ContactApi, health::HealthApi, meta::MetaApi) { - (self.contact, self.health, self.meta) - } -} diff --git a/backend/src/settings.rs b/backend/src/settings.rs deleted file mode 100644 index 94c1a1b..0000000 --- a/backend/src/settings.rs +++ /dev/null @@ -1,619 +0,0 @@ -//! Application configuration settings. -//! -//! This module provides configuration structures that can be loaded from: -//! - YAML configuration files (base.yaml and environment-specific files) -//! - Environment variables (prefixed with APP__) -//! -//! Settings include application details, email server configuration, and environment settings. - -/// Application configuration settings. -/// -/// Loads configuration from YAML files and environment variables. -#[derive(Debug, serde::Deserialize, Clone, Default)] -pub struct Settings { - /// Application-specific settings (name, version, host, port, etc.) - pub application: ApplicationSettings, - /// Debug mode flag - pub debug: bool, - /// Email server configuration for contact form - pub email: EmailSettings, - /// Frontend URL for CORS configuration - pub frontend_url: String, - /// Rate limiting configuration - #[serde(default)] - pub rate_limit: RateLimitSettings, -} - -impl Settings { - /// Creates a new `Settings` instance by loading configuration from files and environment variables. - /// - /// # Errors - /// - /// Returns a `config::ConfigError` if: - /// - Configuration files cannot be read or parsed - /// - Required configuration values are missing - /// - Configuration values cannot be deserialized into the expected types - /// - /// # Panics - /// - /// Panics if: - /// - The current directory cannot be determined - /// - The `APP_ENVIRONMENT` variable contains an invalid value (not "dev", "development", "prod", or "production") - pub fn new() -> Result { - let base_path = std::env::current_dir().expect("Failed to determine the current directory"); - let settings_directory = base_path.join("settings"); - let environment: Environment = std::env::var("APP_ENVIRONMENT") - .unwrap_or_else(|_| "dev".into()) - .try_into() - .expect("Failed to parse APP_ENVIRONMENT"); - let environment_filename = format!("{environment}.yaml"); - // Lower = takes precedence - let settings = config::Config::builder() - .add_source(config::File::from(settings_directory.join("base.yaml"))) - .add_source(config::File::from( - settings_directory.join(environment_filename), - )) - .add_source( - config::Environment::with_prefix("APP") - .prefix_separator("__") - .separator("__"), - ) - .build()?; - settings.try_deserialize() - } -} - -/// Application-specific configuration settings. -#[derive(Debug, serde::Deserialize, Clone, Default)] -pub struct ApplicationSettings { - /// Application name - pub name: String, - /// Application version - pub version: String, - /// Port to bind to - pub port: u16, - /// Host address to bind to - pub host: String, - /// Base URL of the application - pub base_url: String, - /// Protocol (http or https) - pub protocol: String, -} - -/// Application environment. -#[derive(Debug, PartialEq, Eq, Default)] -pub enum Environment { - /// Development environment - #[default] - Development, - /// Production environment - Production, -} - -impl std::fmt::Display for Environment { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let self_str = match self { - Self::Development => "development", - Self::Production => "production", - }; - write!(f, "{self_str}") - } -} - -impl TryFrom for Environment { - type Error = String; - - fn try_from(value: String) -> Result { - Self::try_from(value.as_str()) - } -} - -impl TryFrom<&str> for Environment { - type Error = String; - - fn try_from(value: &str) -> Result { - match value.to_lowercase().as_str() { - "development" | "dev" => Ok(Self::Development), - "production" | "prod" => Ok(Self::Production), - other => Err(format!( - "{other} is not a supported environment. Use either `development` or `production`" - )), - } - } -} - -/// Email server configuration for the contact form. -#[derive(serde::Deserialize, Clone, Default)] -pub struct EmailSettings { - /// SMTP server hostname - pub host: String, - /// SMTP server port - pub port: u16, - /// SMTP authentication username - pub user: String, - /// Email address to send from - pub from: String, - /// SMTP authentication password - pub password: String, - /// Email address to send contact form submissions to - pub recipient: String, - /// STARTTLS configuration - pub starttls: Starttls, - /// Whether to use implicit TLS (SMTPS) - pub tls: bool, -} - -impl std::fmt::Debug for EmailSettings { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("EmailSettings") - .field("host", &self.host) - .field("port", &self.port) - .field("user", &self.user) - .field("from", &self.from) - .field("password", &"[REDACTED]") - .field("recipient", &self.recipient) - .field("starttls", &self.starttls) - .field("tls", &self.tls) - .finish() - } -} - -/// STARTTLS configuration for SMTP connections. -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub enum Starttls { - /// Never use STARTTLS (unencrypted connection) - #[default] - Never, - /// Use STARTTLS if available (opportunistic encryption) - Opportunistic, - /// Always use STARTTLS (required encryption) - Always, -} - -impl TryFrom<&str> for Starttls { - type Error = String; - - fn try_from(value: &str) -> Result { - match value.to_lowercase().as_str() { - "off" | "no" | "never" => Ok(Self::Never), - "opportunistic" => Ok(Self::Opportunistic), - "yes" | "always" => Ok(Self::Always), - other => Err(format!( - "{other} is not a supported option. Use either `yes`, `no`, or `opportunistic`" - )), - } - } -} - -impl TryFrom for Starttls { - type Error = String; - fn try_from(value: String) -> Result { - value.as_str().try_into() - } -} - -impl From for Starttls { - fn from(value: bool) -> Self { - if value { Self::Always } else { Self::Never } - } -} - -impl std::fmt::Display for Starttls { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let self_str = match self { - Self::Never => "never", - Self::Opportunistic => "opportunistic", - Self::Always => "always", - }; - write!(f, "{self_str}") - } -} - -impl<'de> serde::Deserialize<'de> for Starttls { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - struct StartlsVisitor; - - impl serde::de::Visitor<'_> for StartlsVisitor { - type Value = Starttls; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a string or boolean representing STARTTLS setting (e.g., 'yes', 'no', 'opportunistic', true, false)") - } - - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - Starttls::try_from(value).map_err(E::custom) - } - - fn visit_string(self, value: String) -> Result - where - E: serde::de::Error, - { - Starttls::try_from(value.as_str()).map_err(E::custom) - } - - fn visit_bool(self, value: bool) -> Result - where - E: serde::de::Error, - { - Ok(Starttls::from(value)) - } - } - - deserializer.deserialize_any(StartlsVisitor) - } -} - -/// Rate limiting configuration. -#[derive(Debug, serde::Deserialize, Clone)] -pub struct RateLimitSettings { - /// Whether rate limiting is enabled - #[serde(default = "default_rate_limit_enabled")] - pub enabled: bool, - /// Maximum number of requests allowed in the time window (burst size) - #[serde(default = "default_burst_size")] - pub burst_size: u32, - /// Time window in seconds for rate limiting - #[serde(default = "default_per_seconds")] - pub per_seconds: u64, -} - -impl Default for RateLimitSettings { - fn default() -> Self { - Self { - enabled: default_rate_limit_enabled(), - burst_size: default_burst_size(), - per_seconds: default_per_seconds(), - } - } -} - -const fn default_rate_limit_enabled() -> bool { - true -} - -const fn default_burst_size() -> u32 { - 100 -} - -const fn default_per_seconds() -> u64 { - 60 -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn environment_display_development() { - let env = Environment::Development; - assert_eq!(env.to_string(), "development"); - } - - #[test] - fn environment_display_production() { - let env = Environment::Production; - assert_eq!(env.to_string(), "production"); - } - - #[test] - fn environment_from_str_development() { - assert_eq!( - Environment::try_from("development").unwrap(), - Environment::Development - ); - assert_eq!( - Environment::try_from("dev").unwrap(), - Environment::Development - ); - assert_eq!( - Environment::try_from("Development").unwrap(), - Environment::Development - ); - assert_eq!( - Environment::try_from("DEV").unwrap(), - Environment::Development - ); - } - - #[test] - fn environment_from_str_production() { - assert_eq!( - Environment::try_from("production").unwrap(), - Environment::Production - ); - assert_eq!( - Environment::try_from("prod").unwrap(), - Environment::Production - ); - assert_eq!( - Environment::try_from("Production").unwrap(), - Environment::Production - ); - assert_eq!( - Environment::try_from("PROD").unwrap(), - Environment::Production - ); - } - - #[test] - fn environment_from_str_invalid() { - let result = Environment::try_from("invalid"); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("not a supported environment")); - } - - #[test] - fn environment_from_string_development() { - assert_eq!( - Environment::try_from("development".to_string()).unwrap(), - Environment::Development - ); - } - - #[test] - fn environment_from_string_production() { - assert_eq!( - Environment::try_from("production".to_string()).unwrap(), - Environment::Production - ); - } - - #[test] - fn environment_from_string_invalid() { - let result = Environment::try_from("invalid".to_string()); - assert!(result.is_err()); - } - - #[test] - fn environment_default_is_development() { - let env = Environment::default(); - assert_eq!(env, Environment::Development); - } - - #[test] - fn startls_deserialize_from_string_never() { - let json = r#""never""#; - let result: Starttls = serde_json::from_str(json).unwrap(); - assert_eq!(result, Starttls::Never); - - let json = r#""no""#; - let result: Starttls = serde_json::from_str(json).unwrap(); - assert_eq!(result, Starttls::Never); - - let json = r#""off""#; - let result: Starttls = serde_json::from_str(json).unwrap(); - assert_eq!(result, Starttls::Never); - } - - #[test] - fn startls_deserialize_from_string_always() { - let json = r#""always""#; - let result: Starttls = serde_json::from_str(json).unwrap(); - assert_eq!(result, Starttls::Always); - - let json = r#""yes""#; - let result: Starttls = serde_json::from_str(json).unwrap(); - assert_eq!(result, Starttls::Always); - } - - #[test] - fn startls_deserialize_from_string_opportunistic() { - let json = r#""opportunistic""#; - let result: Starttls = serde_json::from_str(json).unwrap(); - assert_eq!(result, Starttls::Opportunistic); - } - - #[test] - fn startls_deserialize_from_bool() { - let json = "true"; - let result: Starttls = serde_json::from_str(json).unwrap(); - assert_eq!(result, Starttls::Always); - - let json = "false"; - let result: Starttls = serde_json::from_str(json).unwrap(); - assert_eq!(result, Starttls::Never); - } - - #[test] - fn startls_deserialize_from_string_invalid() { - let json = r#""invalid""#; - let result: Result = serde_json::from_str(json); - assert!(result.is_err()); - } - - #[test] - fn startls_default_is_never() { - let startls = Starttls::default(); - assert_eq!(startls, Starttls::Never); - } - - #[test] - fn startls_try_from_str_never() { - assert_eq!(Starttls::try_from("never").unwrap(), Starttls::Never); - assert_eq!(Starttls::try_from("no").unwrap(), Starttls::Never); - assert_eq!(Starttls::try_from("off").unwrap(), Starttls::Never); - assert_eq!(Starttls::try_from("NEVER").unwrap(), Starttls::Never); - assert_eq!(Starttls::try_from("No").unwrap(), Starttls::Never); - } - - #[test] - fn startls_try_from_str_always() { - assert_eq!(Starttls::try_from("always").unwrap(), Starttls::Always); - assert_eq!(Starttls::try_from("yes").unwrap(), Starttls::Always); - assert_eq!(Starttls::try_from("ALWAYS").unwrap(), Starttls::Always); - assert_eq!(Starttls::try_from("Yes").unwrap(), Starttls::Always); - } - - #[test] - fn startls_try_from_str_opportunistic() { - assert_eq!( - Starttls::try_from("opportunistic").unwrap(), - Starttls::Opportunistic - ); - assert_eq!( - Starttls::try_from("OPPORTUNISTIC").unwrap(), - Starttls::Opportunistic - ); - } - - #[test] - fn startls_try_from_str_invalid() { - let result = Starttls::try_from("invalid"); - assert!(result.is_err()); - assert!(result - .unwrap_err() - .contains("not a supported option")); - } - - #[test] - fn startls_try_from_string_never() { - assert_eq!( - Starttls::try_from("never".to_string()).unwrap(), - Starttls::Never - ); - } - - #[test] - fn startls_try_from_string_always() { - assert_eq!( - Starttls::try_from("yes".to_string()).unwrap(), - Starttls::Always - ); - } - - #[test] - fn startls_try_from_string_opportunistic() { - assert_eq!( - Starttls::try_from("opportunistic".to_string()).unwrap(), - Starttls::Opportunistic - ); - } - - #[test] - fn startls_try_from_string_invalid() { - let result = Starttls::try_from("invalid".to_string()); - assert!(result.is_err()); - } - - #[test] - fn startls_from_bool_true() { - assert_eq!(Starttls::from(true), Starttls::Always); - } - - #[test] - fn startls_from_bool_false() { - assert_eq!(Starttls::from(false), Starttls::Never); - } - - #[test] - fn startls_display_never() { - let startls = Starttls::Never; - assert_eq!(startls.to_string(), "never"); - } - - #[test] - fn startls_display_always() { - let startls = Starttls::Always; - assert_eq!(startls.to_string(), "always"); - } - - #[test] - fn startls_display_opportunistic() { - let startls = Starttls::Opportunistic; - assert_eq!(startls.to_string(), "opportunistic"); - } - - #[test] - fn rate_limit_settings_default() { - let settings = RateLimitSettings::default(); - assert!(settings.enabled); - assert_eq!(settings.burst_size, 100); - assert_eq!(settings.per_seconds, 60); - } - - #[test] - fn rate_limit_settings_deserialize_full() { - let json = r#"{"enabled": true, "burst_size": 50, "per_seconds": 30}"#; - let settings: RateLimitSettings = serde_json::from_str(json).unwrap(); - assert!(settings.enabled); - assert_eq!(settings.burst_size, 50); - assert_eq!(settings.per_seconds, 30); - } - - #[test] - fn rate_limit_settings_deserialize_partial() { - let json = r#"{"enabled": false}"#; - let settings: RateLimitSettings = serde_json::from_str(json).unwrap(); - assert!(!settings.enabled); - assert_eq!(settings.burst_size, 100); // default - assert_eq!(settings.per_seconds, 60); // default - } - - #[test] - fn rate_limit_settings_deserialize_empty() { - let json = "{}"; - let settings: RateLimitSettings = serde_json::from_str(json).unwrap(); - assert!(settings.enabled); // default - assert_eq!(settings.burst_size, 100); // default - assert_eq!(settings.per_seconds, 60); // default - } - - #[test] - fn startls_deserialize_from_incompatible_type() { - // Test that deserialization from an array fails with expected error message - let json = "[1, 2, 3]"; - let result: Result = serde_json::from_str(json); - assert!(result.is_err()); - let error = result.unwrap_err().to_string(); - // The error should mention what was expected - assert!( - error.contains("STARTTLS") || error.contains("string") || error.contains("boolean") - ); - } - - #[test] - fn startls_deserialize_from_number() { - // Test that deserialization from a number fails - let json = "42"; - let result: Result = serde_json::from_str(json); - assert!(result.is_err()); - } - - #[test] - fn startls_deserialize_from_object() { - // Test that deserialization from an object fails - let json = r#"{"foo": "bar"}"#; - let result: Result = serde_json::from_str(json); - assert!(result.is_err()); - } - - #[test] - fn email_settings_debug_redacts_password() { - let settings = EmailSettings { - host: "smtp.example.com".to_string(), - port: 587, - user: "user@example.com".to_string(), - from: "noreply@example.com".to_string(), - password: "super_secret_password".to_string(), - recipient: "admin@example.com".to_string(), - starttls: Starttls::Always, - tls: false, - }; - - let debug_output = format!("{settings:?}"); - - // Password should be redacted - assert!(debug_output.contains("[REDACTED]")); - // Password should not appear in output - assert!(!debug_output.contains("super_secret_password")); - // Other fields should still be present - assert!(debug_output.contains("smtp.example.com")); - assert!(debug_output.contains("user@example.com")); - } -} diff --git a/backend/src/startup.rs b/backend/src/startup.rs deleted file mode 100644 index fc25792..0000000 --- a/backend/src/startup.rs +++ /dev/null @@ -1,228 +0,0 @@ -//! Application startup and server configuration. -//! -//! This module handles: -//! - Building the application with routes and middleware -//! - Setting up the OpenAPI service and Swagger UI -//! - Configuring CORS -//! - Starting the HTTP server - -use poem::middleware::{AddDataEndpoint, Cors, CorsEndpoint}; -use poem::{EndpointExt, Route}; -use poem_openapi::OpenApiService; - -use crate::{ - middleware::rate_limit::{RateLimit, RateLimitConfig}, - route::Api, - settings::Settings, -}; - -use crate::middleware::rate_limit::RateLimitEndpoint; - -type Server = poem::Server, std::convert::Infallible>; -/// The configured application with rate limiting, CORS, and settings data. -pub type App = AddDataEndpoint>, Settings>; - -/// Application builder that holds the server configuration before running. -pub struct Application { - server: Server, - app: poem::Route, - host: String, - port: u16, - settings: Settings, -} - -/// A fully configured application ready to run. -pub struct RunnableApplication { - server: Server, - app: App, -} - -impl RunnableApplication { - /// Runs the application server. - /// - /// # Errors - /// - /// Returns a `std::io::Error` if the server fails to start or encounters - /// an I/O error during runtime (e.g., port already in use, network issues). - pub async fn run(self) -> Result<(), std::io::Error> { - self.server.run(self.app).await - } -} - -impl From for App { - fn from(value: RunnableApplication) -> Self { - value.app - } -} - -impl From for RunnableApplication { - fn from(value: Application) -> Self { - // Configure rate limiting based on settings - let rate_limit_config = if value.settings.rate_limit.enabled { - tracing::event!( - target: "backend::startup", - tracing::Level::INFO, - burst_size = value.settings.rate_limit.burst_size, - per_seconds = value.settings.rate_limit.per_seconds, - "Rate limiting enabled" - ); - RateLimitConfig::new( - value.settings.rate_limit.burst_size, - value.settings.rate_limit.per_seconds, - ) - } else { - tracing::event!( - target: "backend::startup", - tracing::Level::INFO, - "Rate limiting disabled (using very high limits)" - ); - // Use very high limits to effectively disable rate limiting - RateLimitConfig::new(u32::MAX, 1) - }; - - let app = value - .app - .with(RateLimit::new(&rate_limit_config)) - .with(Cors::new()) - .data(value.settings); - - let server = value.server; - Self { server, app } - } -} - -impl Application { - fn setup_app(settings: &Settings) -> poem::Route { - let api_service = OpenApiService::new( - Api::from(settings).apis(), - settings.application.clone().name, - settings.application.clone().version, - ) - .url_prefix("/api"); - let ui = api_service.swagger_ui(); - poem::Route::new() - .nest("/api", api_service.clone()) - .nest("/specs", api_service.spec_endpoint_yaml()) - .nest("/", ui) - } - - fn setup_server( - settings: &Settings, - tcp_listener: Option>, - ) -> Server { - let tcp_listener = tcp_listener.unwrap_or_else(|| { - let address = format!( - "{}:{}", - settings.application.host, settings.application.port - ); - poem::listener::TcpListener::bind(address) - }); - poem::Server::new(tcp_listener) - } - - /// Builds a new application with the given settings and optional TCP listener. - /// - /// If no listener is provided, one will be created based on the settings. - #[must_use] - pub fn build( - settings: Settings, - tcp_listener: Option>, - ) -> Self { - let port = settings.application.port; - let host = settings.application.clone().host; - let app = Self::setup_app(&settings); - let server = Self::setup_server(&settings, tcp_listener); - Self { - server, - app, - host, - port, - settings, - } - } - - /// Converts the application into a runnable application. - #[must_use] - pub fn make_app(self) -> RunnableApplication { - self.into() - } - - /// Returns the host address the application is configured to bind to. - #[must_use] - pub fn host(&self) -> String { - self.host.clone() - } - - /// Returns the port the application is configured to bind to. - #[must_use] - pub const fn port(&self) -> u16 { - self.port - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn create_test_settings() -> Settings { - Settings { - application: crate::settings::ApplicationSettings { - name: "test-app".to_string(), - version: "1.0.0".to_string(), - port: 8080, - host: "127.0.0.1".to_string(), - base_url: "http://localhost:8080".to_string(), - protocol: "http".to_string(), - }, - debug: false, - email: crate::settings::EmailSettings::default(), - frontend_url: "http://localhost:3000".to_string(), - rate_limit: crate::settings::RateLimitSettings { - enabled: false, - burst_size: 100, - per_seconds: 60, - }, - } - } - - #[test] - fn application_build_and_host() { - let settings = create_test_settings(); - let app = Application::build(settings.clone(), None); - assert_eq!(app.host(), settings.application.host); - } - - #[test] - fn application_build_and_port() { - let settings = create_test_settings(); - let app = Application::build(settings, None); - assert_eq!(app.port(), 8080); - } - - #[test] - fn application_host_returns_correct_value() { - let settings = create_test_settings(); - let app = Application::build(settings, None); - assert_eq!(app.host(), "127.0.0.1"); - } - - #[test] - fn application_port_returns_correct_value() { - let settings = create_test_settings(); - let app = Application::build(settings, None); - assert_eq!(app.port(), 8080); - } - - #[test] - fn application_with_custom_listener() { - let settings = create_test_settings(); - let tcp_listener = - std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); - let port = tcp_listener.local_addr().unwrap().port(); - let listener = poem::listener::TcpListener::bind(format!("127.0.0.1:{port}")); - - let app = Application::build(settings, Some(listener)); - assert_eq!(app.host(), "127.0.0.1"); - assert_eq!(app.port(), 8080); - } -} diff --git a/backend/src/telemetry.rs b/backend/src/telemetry.rs deleted file mode 100644 index 70c3c21..0000000 --- a/backend/src/telemetry.rs +++ /dev/null @@ -1,69 +0,0 @@ -//! Logging and tracing configuration. -//! -//! This module provides utilities for setting up structured logging using the tracing crate. -//! Supports both pretty-printed logs for development and JSON logs for production. - -use tracing_subscriber::layer::SubscriberExt; - -/// Creates a tracing subscriber configured for the given debug mode. -/// -/// In debug mode, logs are pretty-printed to stdout. -/// In production mode, logs are output as JSON. -#[must_use] -pub fn get_subscriber(debug: bool) -> impl tracing::Subscriber + Send + Sync { - let env_filter = if debug { "debug" } else { "info" }.to_string(); - let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(env_filter)); - let stdout_log = tracing_subscriber::fmt::layer().pretty(); - let subscriber = tracing_subscriber::Registry::default() - .with(env_filter) - .with(stdout_log); - let json_log = if debug { - None - } else { - Some(tracing_subscriber::fmt::layer().json()) - }; - subscriber.with(json_log) -} - -/// Initializes the global tracing subscriber. -/// -/// # Panics -/// -/// Panics if: -/// - A global subscriber has already been set -/// - The subscriber cannot be set as the global default -pub fn init_subscriber(subscriber: impl tracing::Subscriber + Send + Sync) { - tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber"); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn get_subscriber_debug_mode() { - let subscriber = get_subscriber(true); - // If we can create the subscriber without panicking, the test passes - // We can't easily inspect the subscriber's internals, but we can verify it's created - let _ = subscriber; - } - - #[test] - fn get_subscriber_production_mode() { - let subscriber = get_subscriber(false); - // If we can create the subscriber without panicking, the test passes - let _ = subscriber; - } - - #[test] - fn get_subscriber_creates_valid_subscriber() { - // Test both debug and non-debug modes create valid subscribers - let debug_subscriber = get_subscriber(true); - let prod_subscriber = get_subscriber(false); - - // Basic smoke test - if these are created without panicking, they're valid - let _ = debug_subscriber; - let _ = prod_subscriber; - } -} diff --git a/frontend/content.config.ts b/content.config.ts similarity index 100% rename from frontend/content.config.ts rename to content.config.ts diff --git a/frontend/content/en/index.md b/content/en/index.md similarity index 100% rename from frontend/content/en/index.md rename to content/en/index.md diff --git a/frontend/content/en/resume.json b/content/en/resume.json similarity index 100% rename from frontend/content/en/resume.json rename to content/en/resume.json diff --git a/frontend/content/en/vocal-synthesis.json b/content/en/vocal-synthesis.json similarity index 100% rename from frontend/content/en/vocal-synthesis.json rename to content/en/vocal-synthesis.json diff --git a/frontend/content/en/vocal-synthesis.md b/content/en/vocal-synthesis.md similarity index 100% rename from frontend/content/en/vocal-synthesis.md rename to content/en/vocal-synthesis.md diff --git a/frontend/content/fr/index.md b/content/fr/index.md similarity index 100% rename from frontend/content/fr/index.md rename to content/fr/index.md diff --git a/frontend/content/fr/resume.json b/content/fr/resume.json similarity index 100% rename from frontend/content/fr/resume.json rename to content/fr/resume.json diff --git a/frontend/content/fr/vocal-synthesis.json b/content/fr/vocal-synthesis.json similarity index 100% rename from frontend/content/fr/vocal-synthesis.json rename to content/fr/vocal-synthesis.json diff --git a/frontend/content/fr/vocal-synthesis.md b/content/fr/vocal-synthesis.md similarity index 100% rename from frontend/content/fr/vocal-synthesis.md rename to content/fr/vocal-synthesis.md diff --git a/frontend/eslint.config.mjs b/eslint.config.mjs similarity index 100% rename from frontend/eslint.config.mjs rename to eslint.config.mjs diff --git a/flake.lock b/flake.lock index 1922f19..b6cbb07 100644 --- a/flake.lock +++ b/flake.lock @@ -68,11 +68,11 @@ ] }, "locked": { - "lastModified": 1761922975, - "narHash": "sha256-j4EB5ku/gDm7h7W7A+k70RYj5nUiW/l9wQtXMJUD2hg=", + "lastModified": 1763136231, + "narHash": "sha256-QVtIjPSQ/xVhuXSSENYOYZPfrjjc/W/djuxcJyKxGTw=", "owner": "cachix", "repo": "devenv", - "rev": "c9f0b47815a4895fadac87812de8a4de27e0ace1", + "rev": "4b8c2bbdb4e01ef8c4093ee1224fe21ed5ea1a5e", "type": "github" }, "original": { @@ -140,6 +140,24 @@ "type": "github" } }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "flakeCompat": { "flake": false, "locked": { @@ -264,9 +282,8 @@ "inputs": { "alejandra": "alejandra", "devenv": "devenv", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay", - "systems": "systems" + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" } }, "rust-analyzer-src": { @@ -286,26 +303,6 @@ "type": "github" } }, - "rust-overlay": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762223900, - "narHash": "sha256-caxpESVH71mdrdihYvQZ9rTZPZqW0GyEG9un7MgpyRM=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "cfe1598d69a42a5edb204770e71b8df77efef2c3", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, "systems": { "locked": { "lastModified": 1681028828, diff --git a/flake.nix b/flake.nix index baecf74..e4e9a30 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,7 @@ { inputs = { nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling"; - systems.url = "github:nix-systems/default"; + flake-utils.url = "github:numtide/flake-utils"; alejandra = { url = "github:kamadorueda/alejandra/4.0.0"; inputs.nixpkgs.follows = "nixpkgs"; @@ -10,10 +10,6 @@ url = "github:cachix/devenv"; inputs.nixpkgs.follows = "nixpkgs"; }; - rust-overlay = { - url = "github:oxalica/rust-overlay"; - inputs.nixpkgs.follows = "nixpkgs"; - }; }; nixConfig = { @@ -30,27 +26,18 @@ outputs = { self, nixpkgs, - devenv, - systems, - rust-overlay, alejandra, + flake-utils, ... - } @ inputs: let - forEachSystem = nixpkgs.lib.genAttrs (import systems); - in { - formatter = forEachSystem (system: alejandra.defaultPackage.${system}); - packages = forEachSystem (system: import ./backend/nix/package.nix { inherit rust-overlay inputs system; }); - devShells = forEachSystem ( - system: let - pkgs = nixpkgs.legacyPackages.${system}; - in { - backend = import ./backend/nix/shell.nix { - inherit inputs pkgs system self rust-overlay; - }; - frontend = import ./frontend/shell.nix { - inherit inputs pkgs self; - }; - } - ); - }; + } @ inputs: + flake-utils.lib.eachDefaultSystem ( + system: let + pkgs = nixpkgs.legacyPackages.${system}; + in { + formatter = alejandra.defaultPackage.${system}; + devShell = import ./nix/shell.nix { + inherit inputs pkgs self; + }; + } + ); } diff --git a/frontend/README.org b/frontend/README.org deleted file mode 100644 index caea59a..0000000 --- a/frontend/README.org +++ /dev/null @@ -1,134 +0,0 @@ -#+title: phundrak.com frontend -#+author: Lucien Cartier-Tilet -#+email: lucien@phundrak.com - -This is the frontend of =phundrak.com=, written with Nuxt. - -* Setup - -** Environment -*** Nix Environment -If you use Nix, you can set up your environment using the [[file:flake.nix][=flake.nix=]] -file, which will give you the exact same development environment as I -use. - -#+begin_src bash -nix develop -#+end_src - -If you have [[https://direnv.net/][=direnv=]] installed, you can simply use it to automatically -enable this environment. However, I *strongly* recommend you to read the -content of the =flake.nix= file before doing so, as you should with any -Nix-defined environment you did not create. - -#+begin_src bash -direnv allow . -#+end_src - -*** Required Tools -To be able to work on this project, you need a Javascript package -manager, such as: -- =npm= -- =pnpm= (recommended) -- =yarn= -- =bun= - -In my case, I use pnpm. - -You can skip this if you are already using my Nix environment. - -** Dependencies -Once you have your environment ready, you can now install the -project’s dependencies. - -#+begin_src bash -# npm -npm install - -# pnpm -pnpm install - -# yarn -yarn install - -# bun -bun install -#+end_src - -* Running the Project -You are now ready to start the development server on -=http://localhost:3000=. - -#+begin_src bash -# npm -npm run dev - -# pnpm -pnpm dev - -# yarn -yarn dev - -# bun -bun run dev -#+end_src - -* Production -Once you are satisfied with the project, you can build the application in production mode. - -#+begin_src bash -# npm -npm run build - -# pnpm -pnpm build - -# yarn -yarn build - -# bun -bun run build -#+end_src - -You can preview locally the production build too. - -#+begin_src bash -# npm -npm run preview - -# pnpm -pnpm preview - -# yarn -yarn preview - -# bun -bun run preview -#+end_src - -Check out the [[https://nuxt.com/docs/getting-started/deployment][deployment documentation]] for more information. - -* Known Issues -** =better-sqlite3= self-registration error -If you encounter an error stating that =better-sqlite3= does not -self-register when running =pnpm run dev=, this is typically caused by -the native module being compiled for a different Node.js version. - -*Solution:* Rebuild the native module for your current Node.js version: - -#+begin_src bash -# Rebuild just better-sqlite3 -pnpm rebuild better-sqlite3 - -# Or rebuild all native modules -pnpm rebuild - -# Or reinstall everything (nuclear option) -rm -rf node_modules -pnpm install -#+end_src - -*Why this happens:* =better-sqlite3= contains native C++ code that -needs to be compiled for each specific Node.js version. When you -update Node.js or switch between versions, native modules need to be -rebuilt. diff --git a/frontend/shell.nix b/frontend/shell.nix deleted file mode 100644 index cde1863..0000000 --- a/frontend/shell.nix +++ /dev/null @@ -1,54 +0,0 @@ -{ - inputs, - pkgs, - self, - ... -}: -inputs.devenv.lib.mkShell { - inherit inputs pkgs; - modules = [ - { - devenv.root = let - devenvRootFileContent = builtins.readFile "${self}/.devenv-root"; - in - pkgs.lib.mkIf (devenvRootFileContent != "") devenvRootFileContent; - } - { - env.PNPM_HOME = "${self}/.pnpm-store"; - - packages = with pkgs; [ - # LSP - marksman - # nodePackages."@tailwindcss/language-server" - # nodePackages."@vue/language-server" - # vscode-langservers-extracted - - rustywind - nodePackages.prettier - nodePackages.eslint - - # Node - nodejs_24 - nodePackages.pnpm - - # typescript - # nodePackages.typescript-language-server - ]; - - enterShell = '' - echo "πŸš€ Nuxt.js development environment loaded!" - echo "πŸ“¦ Node.js version: $(node --version)" - echo "πŸ“¦ pnpm version: $(pnpm --version)" - echo "" - echo "Available LSP servers:" - echo " - typescript-language-server (TypeScript)" - echo " - vue-language-server (Vue/Volar)" - echo " - tailwindcss-language-server (Tailwind CSS)" - echo " - vscode-langservers-extracted (HTML, CSS, JSON, ESLint)" - echo "" - echo "Run 'pnpm install' to install dependencies" - echo "Run 'pnpm dev' to start the development server" - ''; - } - ]; -} diff --git a/frontend/i18n.config.ts b/i18n.config.ts similarity index 100% rename from frontend/i18n.config.ts rename to i18n.config.ts diff --git a/frontend/i18n/locales/en.json b/i18n/locales/en.json similarity index 100% rename from frontend/i18n/locales/en.json rename to i18n/locales/en.json diff --git a/frontend/i18n/locales/fr.json b/i18n/locales/fr.json similarity index 100% rename from frontend/i18n/locales/fr.json rename to i18n/locales/fr.json diff --git a/nix/shell.nix b/nix/shell.nix new file mode 100644 index 0000000..2ed8cf1 --- /dev/null +++ b/nix/shell.nix @@ -0,0 +1,33 @@ +{ + inputs, + pkgs, + self, + ... +}: +inputs.devenv.lib.mkShell { + inherit inputs pkgs; + modules = [ + { + env.PNPM_HOME = "${self}/.pnpm-store"; + + packages = with pkgs; [ + rustywind + nodePackages.prettier + nodePackages.eslint + + # Node + nodejs_24 + nodePackages.pnpm + ]; + + enterShell = '' + echo "πŸš€ Nuxt.js development environment loaded!" + echo "πŸ“¦ Node.js version: $(node --version)" + echo "πŸ“¦ pnpm version: $(pnpm --version)" + echo "" + echo "Run 'pnpm install' to install dependencies" + echo "Run 'pnpm dev' to start the development server" + ''; + } + ]; +} diff --git a/frontend/nuxt.config.ts b/nuxt.config.ts similarity index 100% rename from frontend/nuxt.config.ts rename to nuxt.config.ts diff --git a/frontend/package.json b/package.json similarity index 94% rename from frontend/package.json rename to package.json index 04910ca..149c8d5 100644 --- a/frontend/package.json +++ b/package.json @@ -32,26 +32,23 @@ "nitropack": "^2.12.9", "nuxi": "^3.30.0", "nuxt": "^4.2.0", - "typescript": "^5.9.3", "vite": "^7.1.12", "vue": "^3.5.22", "vue-router": "^4.6.3" }, "devDependencies": { + "@iconify-json/lucide": "^1.2.73", "@iconify-json/material-symbols": "^1.2.44", "@iconify-json/material-symbols-light": "^1.2.44", "@iconify-json/mdi": "^1.2.3", + "@iconify-json/simple-icons": "^1.2.58", "@nuxtjs/i18n": "^10.2.0", "@tailwindcss/postcss": "^4.1.17", "autoprefixer": "^10.4.22", "less": "^4.4.2", "postcss": "^8.5.6", "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", "zod": "^4.1.12" - }, - "pnpm": { - "overrides": { - "sharp": "0.33.4" - } } } diff --git a/frontend/pnpm-lock.yaml b/pnpm-lock.yaml similarity index 97% rename from frontend/pnpm-lock.yaml rename to pnpm-lock.yaml index 30ace5c..1f68bce 100644 --- a/frontend/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,9 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - sharp: 0.33.4 - importers: .: @@ -61,10 +58,7 @@ importers: version: 3.30.0 nuxt: specifier: ^4.2.0 - version: 4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1) - typescript: - specifier: ^5.9.3 - version: 5.9.3 + version: 4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) vite: specifier: ^7.1.12 version: 7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1) @@ -75,15 +69,21 @@ importers: specifier: ^4.6.3 version: 4.6.3(vue@3.5.24(typescript@5.9.3)) devDependencies: + '@iconify-json/lucide': + specifier: ^1.2.73 + version: 1.2.73 '@iconify-json/material-symbols': specifier: ^1.2.44 - version: 1.2.45 + version: 1.2.46 '@iconify-json/material-symbols-light': specifier: ^1.2.44 - version: 1.2.45 + version: 1.2.46 '@iconify-json/mdi': specifier: ^1.2.3 version: 1.2.3 + '@iconify-json/simple-icons': + specifier: ^1.2.58 + version: 1.2.58 '@nuxtjs/i18n': specifier: ^10.2.0 version: 10.2.0(@vue/compiler-dom@3.5.24)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(rollup@4.53.2)(vue@3.5.24(typescript@5.9.3)) @@ -102,6 +102,9 @@ importers: tailwindcss: specifier: ^4.1.17 version: 4.1.17 + typescript: + specifier: ^5.9.3 + version: 5.9.3 zod: specifier: ^4.1.12 version: 4.1.12 @@ -299,11 +302,11 @@ packages: '@dxup/unimport@0.1.2': resolution: {integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==} - '@emnapi/core@1.7.0': - resolution: {integrity: sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==} + '@emnapi/core@1.7.1': + resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} - '@emnapi/runtime@1.7.0': - resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -560,17 +563,23 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@iconify-json/material-symbols-light@1.2.45': - resolution: {integrity: sha512-FtS0UouUSo6cdMZyhp9cDaVWxg14KTV9YjDnt/Dm7LRi59sO+kvKdPOiXZk57kIhE4MUE8qWClNvn+fi5a0pMA==} + '@iconify-json/lucide@1.2.73': + resolution: {integrity: sha512-++HFkqDNu4jqG5+vYT+OcVj9OiuPCw9wQuh8G5QWQnBRSJ9eKwSStiU8ORgOoK07xJsm/0VIHySMniXUUXP9Gw==} - '@iconify-json/material-symbols@1.2.45': - resolution: {integrity: sha512-QGt+57HpuYNYHIjmGRyaCTSStJD4AOj4C6xKFoMd1gtYPzfgK4/MOl3ar3WsgQnAmGo1QuDFOztXIIjDgDO2Kg==} + '@iconify-json/material-symbols-light@1.2.46': + resolution: {integrity: sha512-RxCXAYlxDnR6r1V6/0y1l1UEgLttvQuIt3myTRUrySqOM1DEFyOQ6fnskoBAIK6NsHd0W+3mJGYbTD4LKsHFdg==} + + '@iconify-json/material-symbols@1.2.46': + resolution: {integrity: sha512-cNWdSAa5Z3f0TlqdCt28rmeYWGKwe68J1ORdyHyqC4D6H7CWiVKBJXV3TDTocOQVDO372bz+cmsFeo4+pbRy+A==} '@iconify-json/mdi@1.2.3': resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==} - '@iconify/collections@1.0.617': - resolution: {integrity: sha512-coRyOUT2gQ8SHptjaW5YG2VEscoIcpEAN/mn1UgorKKgoA6l+aoebW58WXLWcvSZ4LKEmim3Hnp/bLMLMoOejg==} + '@iconify-json/simple-icons@1.2.58': + resolution: {integrity: sha512-XtXEoRALqztdNc9ujYBj2tTCPKdIPKJBdLNDebFF46VV1aOAwTbAYMgNsK5GMCpTJupLCmpBWDn+gX5SpECorQ==} + + '@iconify/collections@1.0.618': + resolution: {integrity: sha512-G0pOVenguqtHC3mV07JFqNHJnZl0Mq+Jc8Uksx6tVrq4KktenM5s9yih83FK7PCCCEHccvS/SkD06cfZOvd0Yw==} '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} @@ -583,119 +592,6 @@ packages: peerDependencies: vue: '>=3' - '@img/sharp-darwin-arm64@0.33.4': - resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.33.4': - resolution: {integrity: sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.0.2': - resolution: {integrity: sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==} - engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.0.2': - resolution: {integrity: sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==} - engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.0.2': - resolution: {integrity: sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==} - engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linux-arm@1.0.2': - resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==} - engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.0.2': - resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==} - engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [s390x] - os: [linux] - - '@img/sharp-libvips-linux-x64@1.0.2': - resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==} - engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-arm64@1.0.2': - resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==} - engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-x64@1.0.2': - resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==} - engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-linux-arm64@0.33.4': - resolution: {integrity: sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-linux-arm@0.33.4': - resolution: {integrity: sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==} - engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm] - os: [linux] - - '@img/sharp-linux-s390x@0.33.4': - resolution: {integrity: sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==} - engines: {glibc: '>=2.31', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [s390x] - os: [linux] - - '@img/sharp-linux-x64@0.33.4': - resolution: {integrity: sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==} - engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-linuxmusl-arm64@0.33.4': - resolution: {integrity: sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==} - engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [arm64] - os: [linux] - - '@img/sharp-linuxmusl-x64@0.33.4': - resolution: {integrity: sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==} - engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [linux] - - '@img/sharp-wasm32@0.33.4': - resolution: {integrity: sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [wasm32] - - '@img/sharp-win32-ia32@0.33.4': - resolution: {integrity: sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.33.4': - resolution: {integrity: sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} - cpu: [x64] - os: [win32] - '@internationalized/date@3.10.0': resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==} @@ -2547,6 +2443,36 @@ packages: bare-abort-controller: optional: true + bare-fs@4.5.1: + resolution: {integrity: sha512-zGUCsm3yv/ePt2PHNbVxjjn0nNB1MkIaR4wOCxJ2ig5pCf5cCVAYJXVhQg/3OhhJV6DB1ts7Hv0oUaElc2TPQg==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.7.0: + resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.3.2: + resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2868,8 +2794,8 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.0: + resolution: {integrity: sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==} db0@0.3.4: resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==} @@ -2930,8 +2856,8 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - default-browser-id@5.0.0: - resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} default-browser@5.3.0: @@ -3023,8 +2949,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.250: - resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==} + electron-to-chromium@1.5.252: + resolution: {integrity: sha512-53uTpjtRgS7gjIxZ4qCgFdNO2q+wJt/Z8+xAvxbCqXPJrY6h7ighUkadQmNMXH96crtpa6gPFNP7BF4UBGDuaA==} embla-carousel-auto-height@8.6.0: resolution: {integrity: sha512-/HrJQOEM6aol/oF33gd2QlINcXy3e19fJWvHDuHWp2bpyTa+2dm9tVVJak30m2Qy6QyQ6Fc8DkImtv7pxWOJUQ==} @@ -3900,8 +3826,8 @@ packages: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} - knitwork@1.2.0: - resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==} + knitwork@1.3.0: + resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -4344,10 +4270,13 @@ packages: xml2js: optional: true - node-abi@3.83.0: - resolution: {integrity: sha512-o2PH88PgFlfoSDjU5oq/b/p9m+DJaPfslRI5FzNqcK1ea1i2/8xo/FL850kdgw0EAQJ/cSyyi2W2fBjHBdg5rA==} + node-abi@3.85.0: + resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} engines: {node: '>=10'} + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -5082,9 +5011,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.33.4: - resolution: {integrity: sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==} - engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} + engines: {node: '>=14.15.0'} shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} @@ -5322,6 +5251,9 @@ packages: tar-fs@2.1.4: resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + tar-fs@3.1.1: + resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} @@ -5518,8 +5450,8 @@ packages: '@nuxt/kit': optional: true - unplugin-vue-router@0.16.1: - resolution: {integrity: sha512-7A7gUVzLIYMBrBPKk8l4lZoZXDOrO8+etw6/RTrqG3OzpLUUZEXJFUW7+OyMIpQK93sEbdkR2z9ZNNl/r32FMw==} + unplugin-vue-router@0.16.2: + resolution: {integrity: sha512-lE6ZjnHaXfS2vFI/PSEwdKcdOo5RwAbCKUnPBIN9YwLgSWas3x+qivzQvJa/uxhKzJldE6WK43aDKjGj9Rij9w==} peerDependencies: '@vue/compiler-sfc': ^3.5.17 vue-router: ^4.6.0 @@ -5658,8 +5590,8 @@ packages: peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 - vite-node@5.0.0: - resolution: {integrity: sha512-nJINVH7lHBKoyDFYnwrXbNUrmTJ2ssBHTd/mXVZfLq/O5K7ksv4CayQOA5KkbOSrsgSQg8antcVPgQmzBWWn/w==} + vite-node@5.1.0: + resolution: {integrity: sha512-ci+CXFFrQfRgdO0WDSKNQ28OOglURJUw2hVlfir4IA+Q2nHKmU/qIbmiYO7oB8CZvvSoyCmHycXz5MiX03BrsQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -5804,6 +5736,12 @@ packages: peerDependencies: vue: ^3.5.0 + vue-tsc@3.1.3: + resolution: {integrity: sha512-StMNfZHwPIXQgY3KxPKM0Jsoc8b46mDV3Fn2UlHCBIwRJApjqrSwqeMYgWf0zpN+g857y74pv7GWuBm+UqQe1w==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + vue@3.5.24: resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==} peerDependencies: @@ -6211,13 +6149,13 @@ snapshots: '@dxup/unimport@0.1.2': {} - '@emnapi/core@1.7.0': + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.7.0': + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true @@ -6430,11 +6368,15 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@iconify-json/material-symbols-light@1.2.45': + '@iconify-json/lucide@1.2.73': dependencies: '@iconify/types': 2.0.0 - '@iconify-json/material-symbols@1.2.45': + '@iconify-json/material-symbols-light@1.2.46': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/material-symbols@1.2.46': dependencies: '@iconify/types': 2.0.0 @@ -6442,7 +6384,11 @@ snapshots: dependencies: '@iconify/types': 2.0.0 - '@iconify/collections@1.0.617': + '@iconify-json/simple-icons@1.2.58': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/collections@1.0.618': dependencies: '@iconify/types': 2.0.0 @@ -6466,81 +6412,6 @@ snapshots: '@iconify/types': 2.0.0 vue: 3.5.24(typescript@5.9.3) - '@img/sharp-darwin-arm64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.2 - optional: true - - '@img/sharp-darwin-x64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.2 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.0.2': - optional: true - - '@img/sharp-libvips-darwin-x64@1.0.2': - optional: true - - '@img/sharp-libvips-linux-arm64@1.0.2': - optional: true - - '@img/sharp-libvips-linux-arm@1.0.2': - optional: true - - '@img/sharp-libvips-linux-s390x@1.0.2': - optional: true - - '@img/sharp-libvips-linux-x64@1.0.2': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.0.2': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.0.2': - optional: true - - '@img/sharp-linux-arm64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.2 - optional: true - - '@img/sharp-linux-arm@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.2 - optional: true - - '@img/sharp-linux-s390x@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.2 - optional: true - - '@img/sharp-linux-x64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.2 - optional: true - - '@img/sharp-linuxmusl-arm64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 - optional: true - - '@img/sharp-linuxmusl-x64@0.33.4': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.2 - optional: true - - '@img/sharp-wasm32@0.33.4': - dependencies: - '@emnapi/runtime': 1.7.0 - optional: true - - '@img/sharp-win32-ia32@0.33.4': - optional: true - - '@img/sharp-win32-x64@0.33.4': - optional: true - '@internationalized/date@3.10.0': dependencies: '@swc/helpers': 0.5.17 @@ -6696,15 +6567,15 @@ snapshots: '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.7.0 - '@emnapi/runtime': 1.7.0 + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.1 optional: true '@napi-rs/wasm-runtime@1.0.7': dependencies: - '@emnapi/core': 1.7.0 - '@emnapi/runtime': 1.7.0 + '@emnapi/core': 1.7.1 + '@emnapi/runtime': 1.7.1 '@tybys/wasm-util': 0.10.1 optional: true @@ -6779,7 +6650,7 @@ snapshots: hookable: 5.5.3 jiti: 2.6.1 json-schema-to-typescript: 15.0.4 - knitwork: 1.2.0 + knitwork: 1.3.0 mdast-util-to-hast: 13.2.0 mdast-util-to-string: 4.0.0 micromark: 4.0.2 @@ -7054,7 +6925,7 @@ snapshots: '@nuxt/icon@2.1.0(magicast@0.5.1)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3))': dependencies: - '@iconify/collections': 1.0.617 + '@iconify/collections': 1.0.618 '@iconify/types': 2.0.0 '@iconify/utils': 3.0.2 '@iconify/vue': 5.0.0(vue@3.5.24(typescript@5.9.3)) @@ -7081,7 +6952,7 @@ snapshots: defu: 6.1.4 h3: 1.15.4 image-meta: 0.2.2 - knitwork: 1.2.0 + knitwork: 1.3.0 ohash: 2.0.11 pathe: 2.0.3 std-env: 3.10.0 @@ -7104,10 +6975,13 @@ snapshots: - '@vercel/functions' - '@vercel/kv' - aws4fetch + - bare-abort-controller + - bare-buffer - db0 - idb-keyval - ioredis - magicast + - react-native-b4a - uploadthing '@nuxt/kit@3.20.1(magicast@0.5.1)': @@ -7121,7 +6995,7 @@ snapshots: ignore: 7.0.5 jiti: 2.6.1 klona: 2.0.6 - knitwork: 1.2.0 + knitwork: 1.3.0 mlly: 1.8.0 ohash: 2.0.11 pathe: 2.0.3 @@ -7161,7 +7035,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@4.2.1(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3)': + '@nuxt/nitro-server@4.2.1(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1))(typescript@5.9.3)': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.2.1(magicast@0.5.1) @@ -7179,7 +7053,7 @@ snapshots: klona: 2.0.6 mocked-exports: 0.1.1 nitropack: 2.12.9(better-sqlite3@12.4.1) - nuxt: 4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1) + nuxt: 4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) pathe: 2.0.3 pkg-types: 2.3.0 radix3: 1.1.2 @@ -7355,7 +7229,7 @@ snapshots: embla-carousel-wheel-gestures: 8.1.0(embla-carousel@8.6.0) fuse.js: 7.1.0 hookable: 5.5.3 - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 motion-v: 1.7.4(@vueuse/core@13.9.0(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)) @@ -7419,7 +7293,7 @@ snapshots: - vite - vue - '@nuxt/vite-builder@4.2.1(eslint@9.39.1(jiti@2.6.1))(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))(yaml@2.8.1)': + '@nuxt/vite-builder@4.2.1(eslint@9.39.1(jiti@2.6.1))(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3))(yaml@2.8.1)': dependencies: '@nuxt/kit': 4.2.1(magicast@0.5.1) '@rollup/plugin-replace': 6.0.3(rollup@4.53.2) @@ -7435,11 +7309,11 @@ snapshots: get-port-please: 3.2.0 h3: 1.15.4 jiti: 2.6.1 - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 mocked-exports: 0.1.1 - nuxt: 4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1) + nuxt: 4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1) pathe: 2.0.3 pkg-types: 2.3.0 postcss: 8.5.6 @@ -7449,8 +7323,8 @@ snapshots: ufo: 1.6.1 unenv: 2.0.0-rc.24 vite: 7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1) - vite-node: 5.0.0(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1) - vite-plugin-checker: 0.11.0(eslint@9.39.1(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)) + vite-node: 5.1.0(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1) + vite-plugin-checker: 0.11.0(eslint@9.39.1(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3)) vue: 3.5.24(typescript@5.9.3) vue-bundle-renderer: 2.2.0 transitivePeerDependencies: @@ -7505,7 +7379,7 @@ snapshots: defu: 6.1.4 devalue: 5.5.0 h3: 1.15.4 - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 nuxt-define: 1.0.0 @@ -7517,7 +7391,7 @@ snapshots: typescript: 5.9.3 ufo: 1.6.1 unplugin: 2.3.10 - unplugin-vue-router: 0.16.1(@vue/compiler-sfc@3.5.24)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)) + unplugin-vue-router: 0.16.2(@vue/compiler-sfc@3.5.24)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)) unstorage: 1.17.2(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2) vue-i18n: 11.1.12(vue@3.5.24(typescript@5.9.3)) vue-router: 4.6.3(vue@3.5.24(typescript@5.9.3)) @@ -8625,7 +8499,7 @@ snapshots: '@vue/reactivity': 3.5.24 '@vue/runtime-core': 3.5.24 '@vue/shared': 3.5.24 - csstype: 3.1.3 + csstype: 3.2.0 '@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.9.3))': dependencies: @@ -8810,6 +8684,41 @@ snapshots: bare-events@2.8.2: {} + bare-fs@4.5.1: + dependencies: + bare-events: 2.8.2 + bare-path: 3.0.0 + bare-stream: 2.7.0(bare-events@2.8.2) + bare-url: 2.3.2 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-os@3.6.2: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.2 + optional: true + + bare-stream@2.7.0(bare-events@2.8.2): + dependencies: + streamx: 2.23.0 + optionalDependencies: + bare-events: 2.8.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-url@2.3.2: + dependencies: + bare-path: 3.0.0 + optional: true + base64-js@1.5.1: {} baseline-browser-mapping@2.8.28: {} @@ -8856,7 +8765,7 @@ snapshots: dependencies: baseline-browser-mapping: 2.8.28 caniuse-lite: 1.0.30001754 - electron-to-chromium: 1.5.250 + electron-to-chromium: 1.5.252 node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) @@ -9153,7 +9062,7 @@ snapshots: dependencies: css-tree: 2.2.1 - csstype@3.1.3: {} + csstype@3.2.0: {} db0@0.3.4(better-sqlite3@12.4.1): optionalDependencies: @@ -9181,12 +9090,12 @@ snapshots: deepmerge@4.3.1: {} - default-browser-id@5.0.0: {} + default-browser-id@5.0.1: {} default-browser@5.3.0: dependencies: bundle-name: 4.1.0 - default-browser-id: 5.0.0 + default-browser-id: 5.0.1 define-lazy-prop@2.0.0: {} @@ -9250,7 +9159,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.250: {} + electron-to-chromium@1.5.252: {} embla-carousel-auto-height@8.6.0(embla-carousel@8.6.0): dependencies: @@ -10124,7 +10033,7 @@ snapshots: listhen: 1.9.0 ofetch: 1.5.1 pathe: 1.1.2 - sharp: 0.33.4 + sharp: 0.32.6 svgo: 3.3.2 ufo: 1.6.1 unstorage: 1.17.2(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2) @@ -10145,9 +10054,12 @@ snapshots: - '@vercel/functions' - '@vercel/kv' - aws4fetch + - bare-abort-controller + - bare-buffer - db0 - idb-keyval - ioredis + - react-native-b4a - uploadthing optional: true @@ -10310,7 +10222,7 @@ snapshots: klona@2.0.6: {} - knitwork@1.2.0: {} + knitwork@1.3.0: {} kolorist@1.8.0: {} @@ -10944,7 +10856,7 @@ snapshots: ioredis: 5.8.2 jiti: 2.6.1 klona: 2.0.6 - knitwork: 1.2.0 + knitwork: 1.3.0 listhen: 1.9.0 magic-string: 0.30.21 magicast: 0.5.1 @@ -11009,10 +10921,13 @@ snapshots: - supports-color - uploadthing - node-abi@3.83.0: + node-abi@3.85.0: dependencies: semver: 7.7.3 + node-addon-api@6.1.0: + optional: true + node-addon-api@7.1.1: {} node-emoji@2.2.0: @@ -11075,16 +10990,16 @@ snapshots: nuxt-define@1.0.0: {} - nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1): + nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1): dependencies: '@dxup/nuxt': 0.2.2(magicast@0.5.1) '@nuxt/cli': 3.30.0(magicast@0.5.1) '@nuxt/devtools': 3.1.0(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue@3.5.24(typescript@5.9.3)) '@nuxt/kit': 4.2.1(magicast@0.5.1) - '@nuxt/nitro-server': 4.2.1(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3) + '@nuxt/nitro-server': 4.2.1(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1))(typescript@5.9.3) '@nuxt/schema': 4.2.1 '@nuxt/telemetry': 2.6.6(magicast@0.5.1) - '@nuxt/vite-builder': 4.2.1(eslint@9.39.1(jiti@2.6.1))(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))(yaml@2.8.1) + '@nuxt/vite-builder': 4.2.1(eslint@9.39.1(jiti@2.6.1))(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.24)(better-sqlite3@12.4.1)(db0@0.3.4(better-sqlite3@12.4.1))(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(less@4.4.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3))(yaml@2.8.1))(optionator@0.9.4)(rollup@4.53.2)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@3.1.3(typescript@5.9.3))(vue@3.5.24(typescript@5.9.3))(yaml@2.8.1) '@unhead/vue': 2.0.19(vue@3.5.24(typescript@5.9.3)) '@vue/shared': 3.5.24 c12: 3.3.2(magicast@0.5.1) @@ -11104,7 +11019,7 @@ snapshots: impound: 1.0.0 jiti: 2.6.1 klona: 2.0.6 - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 nanotar: 0.2.0 @@ -11130,7 +11045,7 @@ snapshots: unctx: 2.4.1 unimport: 5.5.0 unplugin: 2.3.10 - unplugin-vue-router: 0.16.1(@vue/compiler-sfc@3.5.24)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)) + unplugin-vue-router: 0.16.2(@vue/compiler-sfc@3.5.24)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)) untyped: 2.0.0 vue: 3.5.24(typescript@5.9.3) vue-router: 4.6.3(vue@3.5.24(typescript@5.9.3)) @@ -11647,7 +11562,7 @@ snapshots: minimist: 1.2.8 mkdirp-classic: 0.5.3 napi-build-utils: 2.0.0 - node-abi: 3.83.0 + node-abi: 3.85.0 pump: 3.0.3 rc: 1.2.8 simple-get: 4.0.1 @@ -12024,31 +11939,20 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.33.4: + sharp@0.32.6: dependencies: color: 4.2.3 detect-libc: 2.1.2 + node-addon-api: 6.1.0 + prebuild-install: 7.1.3 semver: 7.7.3 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.4 - '@img/sharp-darwin-x64': 0.33.4 - '@img/sharp-libvips-darwin-arm64': 1.0.2 - '@img/sharp-libvips-darwin-x64': 1.0.2 - '@img/sharp-libvips-linux-arm': 1.0.2 - '@img/sharp-libvips-linux-arm64': 1.0.2 - '@img/sharp-libvips-linux-s390x': 1.0.2 - '@img/sharp-libvips-linux-x64': 1.0.2 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 - '@img/sharp-libvips-linuxmusl-x64': 1.0.2 - '@img/sharp-linux-arm': 0.33.4 - '@img/sharp-linux-arm64': 0.33.4 - '@img/sharp-linux-s390x': 0.33.4 - '@img/sharp-linux-x64': 0.33.4 - '@img/sharp-linuxmusl-arm64': 0.33.4 - '@img/sharp-linuxmusl-x64': 0.33.4 - '@img/sharp-wasm32': 0.33.4 - '@img/sharp-win32-ia32': 0.33.4 - '@img/sharp-win32-x64': 0.33.4 + simple-get: 4.0.1 + tar-fs: 3.1.1 + tunnel-agent: 0.6.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a optional: true shebang-command@2.0.0: @@ -12291,6 +12195,19 @@ snapshots: pump: 3.0.3 tar-stream: 2.2.0 + tar-fs@3.1.1: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.5.1 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + optional: true + tar-stream@2.2.0: dependencies: bl: 4.1.0 @@ -12531,7 +12448,7 @@ snapshots: transitivePeerDependencies: - supports-color - unplugin-vue-router@0.16.1(@vue/compiler-sfc@3.5.24)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)): + unplugin-vue-router@0.16.2(@vue/compiler-sfc@3.5.24)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.24(typescript@5.9.3)))(vue@3.5.24(typescript@5.9.3)): dependencies: '@babel/generator': 7.28.5 '@vue-macros/common': 3.1.1(vue@3.5.24(typescript@5.9.3)) @@ -12613,12 +12530,12 @@ snapshots: citty: 0.1.6 defu: 6.1.4 jiti: 2.6.1 - knitwork: 1.2.0 + knitwork: 1.3.0 scule: 1.3.0 unwasm@0.3.11: dependencies: - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 pathe: 2.0.3 @@ -12628,7 +12545,7 @@ snapshots: unwasm@0.5.0: dependencies: exsolve: 1.0.8 - knitwork: 1.2.0 + knitwork: 1.3.0 magic-string: 0.30.21 mlly: 1.8.0 pathe: 2.0.3 @@ -12685,7 +12602,7 @@ snapshots: dependencies: vite: 7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1) - vite-node@5.0.0(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1): + vite-node@5.1.0(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 @@ -12706,7 +12623,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.11.0(eslint@9.39.1(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)): + vite-plugin-checker@0.11.0(eslint@9.39.1(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1))(vue-tsc@3.1.3(typescript@5.9.3)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -12721,6 +12638,7 @@ snapshots: eslint: 9.39.1(jiti@2.6.1) optionator: 0.9.4 typescript: 5.9.3 + vue-tsc: 3.1.3(typescript@5.9.3) vite-plugin-inspect@11.3.3(@nuxt/kit@4.2.1(magicast@0.5.1))(vite@7.2.2(jiti@2.6.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.44.1)(yaml@2.8.1)): dependencies: @@ -12828,6 +12746,13 @@ snapshots: '@vue/devtools-api': 6.6.4 vue: 3.5.24(typescript@5.9.3) + vue-tsc@3.1.3(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.23 + '@vue/language-core': 3.1.3(typescript@5.9.3) + typescript: 5.9.3 + optional: true + vue@3.5.24(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.5.24 diff --git a/frontend/pnpm-workspace.yaml b/pnpm-workspace.yaml similarity index 100% rename from frontend/pnpm-workspace.yaml rename to pnpm-workspace.yaml diff --git a/frontend/public/favicon.ico b/public/favicon.ico similarity index 100% rename from frontend/public/favicon.ico rename to public/favicon.ico diff --git a/frontend/public/robots.txt b/public/robots.txt similarity index 100% rename from frontend/public/robots.txt rename to public/robots.txt diff --git a/frontend/tsconfig.json b/tsconfig.json similarity index 75% rename from frontend/tsconfig.json rename to tsconfig.json index 307b213..f3a01e1 100644 --- a/frontend/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,13 @@ { // https://nuxt.com/docs/guide/concepts/typescript "files": [], + "exclude": [ + ".jj", + ".git", + "**/.output/*", + "**/node_modules/*", + "**/dist/*" + ], "references": [ { "path": "./.nuxt/tsconfig.app.json"