chore: separate frontend from backend
This commit is contained in:
@@ -1 +1 @@
|
|||||||
/home/phundrak/code/web/phundrak.com
|
/home/phundrak/code/web/phundrak.com-frontend
|
||||||
|
|||||||
@@ -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 <user@example.com>"
|
|
||||||
APP__EMAIL__FROM="Contact Form <noreply@example.com>"
|
|
||||||
NUXT_PUBLIC_BACKEND_URL=http://localhost:3100
|
NUXT_PUBLIC_BACKEND_URL=http://localhost:3100
|
||||||
NUXT_PUBLIC_TURNSTILE_SITE_KEY="changeme"
|
NUXT_PUBLIC_TURNSTILE_SITE_KEY="changeme"
|
||||||
NUXT_TURNSTILE_SECRET_KEY="changeme"
|
NUXT_TURNSTILE_SECRET_KEY="changeme"
|
||||||
|
|||||||
36
.envrc
36
.envrc
@@ -12,43 +12,13 @@ dotenv_if_exists
|
|||||||
watch_file flake.nix
|
watch_file flake.nix
|
||||||
watch_file flake.lock
|
watch_file flake.lock
|
||||||
watch_file .envrc.local
|
watch_file .envrc.local
|
||||||
watch_file backend/shell.nix
|
watch_file nix/shell.nix
|
||||||
watch_file frontend/shell.nix
|
|
||||||
|
|
||||||
# Check if .envrc.local exists and contains a shell preference
|
# Check if .envrc.local exists and contains a shell preference
|
||||||
if [[ -f .envrc.local ]]; then
|
if [[ -f .envrc.local ]]; then
|
||||||
source .envrc.local
|
source .envrc.local
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If no shell is specified, prompt the user interactively
|
if ! use flake . --no-pure-eval; then
|
||||||
if [[ -z "$NIX_SHELL_NAME" ]]; 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
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
|||||||
*.org linguist-detectable=true
|
|
||||||
217
.github/workflows/README.md
vendored
217
.github/workflows/README.md
vendored
@@ -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<number>` | `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
|
|
||||||
123
.github/workflows/publish-docker.yml
vendored
123
.github/workflows/publish-docker.yml
vendored
@@ -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<number>' 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 }}"
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -16,10 +16,6 @@ logs
|
|||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
# Backend
|
|
||||||
target/
|
|
||||||
coverage/
|
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
## Nuxt dev/build outputs
|
## Nuxt dev/build outputs
|
||||||
.output
|
.output
|
||||||
|
|||||||
9
.volarrc
9
.volarrc
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"vueCompilerOptions": {
|
|
||||||
"target": 3.5,
|
|
||||||
"extensions": [".vue"]
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"tsdk": "frontend/node_modules/typescript/lib"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
176
README.org
176
README.org
@@ -1,76 +1,134 @@
|
|||||||
#+title: phundrak.com
|
#+title: phundrak.com frontend
|
||||||
|
#+author: Lucien Cartier-Tilet
|
||||||
|
#+email: lucien@phundrak.com
|
||||||
|
|
||||||
#+html: <a href="https://www.rust-lang.org/"><img src="https://img.shields.io/badge/Rust-Backend-orange.svg?style=flat-square&logo=Rust&logoColor=white" /></a>
|
This is the frontend of =phundrak.com=, written with Nuxt.
|
||||||
#+html: <a href="https://nuxt.com/"><img src="https://img.shields.io/badge/Frontend-Nuxt%204-00DC82?logo=Nuxt.js&logoColor=white&style=flat-square"/></a>
|
|
||||||
#+html: <a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3-42B883?logo=Vue.js&logoColor=white&style=flat-square"/></a>
|
|
||||||
#+html: <a href="https://phundrak.com"><img src="https://img.shields.io/badge/Website-phundrak.com-blue?style=flat-square&logo=buffer" /></a>
|
|
||||||
|
|
||||||
* Introduction
|
* Setup
|
||||||
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]].
|
|
||||||
|
|
||||||
* Architecture
|
** Environment
|
||||||
The website follows a modern full-stack architecture:
|
*** 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/]])
|
#+begin_src bash
|
||||||
- *Frontend*: Nuxt 4 + Vue 3 + TypeScript (located in [[file:frontend/][frontend/]])
|
nix develop
|
||||||
|
|
||||||
** 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
|
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
To run tests:
|
If you have [[https://direnv.net/][=direnv=]] installed, you can simply use it to automatically
|
||||||
#+begin_src shell
|
enable this environment. However, I *strongly* recommend you to read the
|
||||||
cd backend
|
content of the =flake.nix= file before doing so, as you should with any
|
||||||
cargo test
|
Nix-defined environment you did not create.
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
direnv allow .
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
For continuous testing and linting during development, use [[https://dystroy.org/bacon/][bacon]]:
|
*** Required Tools
|
||||||
#+begin_src shell
|
To be able to work on this project, you need a Javascript package
|
||||||
cd backend
|
manager, such as:
|
||||||
bacon
|
- =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
|
#+end_src
|
||||||
|
|
||||||
*** Building the Backend
|
* Running the Project
|
||||||
To build the backend for production:
|
You are now ready to start the development server on
|
||||||
#+begin_src shell
|
=http://localhost:3000=.
|
||||||
cd backend
|
|
||||||
cargo build --release
|
#+begin_src bash
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run dev
|
||||||
#+end_src
|
#+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
|
#+begin_src bash
|
||||||
The frontend is built with Nuxt 4, Vue 3, and TypeScript, providing a
|
# npm
|
||||||
modern single-page application experience.
|
npm run build
|
||||||
|
|
||||||
*** Installing Dependencies
|
# pnpm
|
||||||
First, install the required dependencies using =pnpm=:
|
pnpm build
|
||||||
#+begin_src shell
|
|
||||||
cd frontend
|
# 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
|
pnpm install
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
*** Running the Frontend
|
*Why this happens:* =better-sqlite3= contains native C++ code that
|
||||||
To run the frontend in development mode:
|
needs to be compiled for each specific Node.js version. When you
|
||||||
#+begin_src shell
|
update Node.js or switch between versions, native modules need to be
|
||||||
cd frontend
|
rebuilt.
|
||||||
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=.
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export const useDataJson = (prefix: string) => {
|
|||||||
} = {},
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
const { useFilter = false, fallbackToEnglish = false, extractMeta = false } = options;
|
const { useFilter = false, fallbackToEnglish = false, extractMeta = false } = options;
|
||||||
|
|
||||||
const { data } = await useAsyncData(
|
const { data } = await useAsyncData(
|
||||||
key.value,
|
key.value,
|
||||||
async () => {
|
async () => {
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
[all]
|
|
||||||
out = ["Xml"]
|
|
||||||
target-dir = "coverage"
|
|
||||||
output-dir = "coverage"
|
|
||||||
fail-under = 60
|
|
||||||
exclude-files = ["target/*"]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
[all]
|
|
||||||
out = ["Html", "Lcov"]
|
|
||||||
skip-clean = true
|
|
||||||
target-dir = "coverage"
|
|
||||||
output-dir = "coverage"
|
|
||||||
fail-under = 60
|
|
||||||
exclude-files = ["target/*"]
|
|
||||||
3249
backend/Cargo.lock
generated
3249
backend/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "phundrak-dot-com-backend"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
publish = false
|
|
||||||
authors = ["Lucien Cartier-Tilet <lucien@phundrak.com>"]
|
|
||||||
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)'] }
|
|
||||||
@@ -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 <noreply@example.com>
|
|
||||||
password: your_password
|
|
||||||
recipient: Admin <admin@example.com>
|
|
||||||
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 <test_name>
|
|
||||||
```
|
|
||||||
|
|
||||||
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 <noreply@example.com>" \
|
|
||||||
-e APP__EMAIL__RECIPIENT="Admin <admin@example.com>" \
|
|
||||||
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`)
|
|
||||||
- `<version>` - Specific version (e.g., `1.0.0`, from tagged commits like `v1.0.0`)
|
|
||||||
- `develop` - Latest development build (from `develop` branch)
|
|
||||||
- `pr<number>` - 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 <noreply@example.com>"
|
|
||||||
APP__EMAIL__RECIPIENT: "Admin <admin@example.com>"
|
|
||||||
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`, `<version>` |
|
|
||||||
| Branch push | Push to `develop` | `develop` |
|
|
||||||
| Pull request | PR opened/updated | `pr<number>` |
|
|
||||||
| 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.
|
|
||||||
@@ -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
|
|
||||||
@@ -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 = []
|
|
||||||
@@ -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:
|
|
||||||
@@ -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 <lucien@phundrak.com>";
|
|
||||||
"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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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)"
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
application:
|
|
||||||
port: 3100
|
|
||||||
version: "0.1.0"
|
|
||||||
|
|
||||||
rate_limit:
|
|
||||||
enabled: true
|
|
||||||
burst_size: 10
|
|
||||||
per_seconds: 60
|
|
||||||
@@ -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 <noreply@example.com>
|
|
||||||
recipient: Admin <user@example.com>
|
|
||||||
tls: false
|
|
||||||
starttls: false
|
|
||||||
@@ -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
|
|
||||||
@@ -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<poem::listener::TcpListener<String>>;
|
|
||||||
|
|
||||||
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<String> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
//! Custom middleware for the application.
|
|
||||||
//!
|
|
||||||
//! This module contains custom middleware implementations including rate limiting.
|
|
||||||
|
|
||||||
pub mod rate_limit;
|
|
||||||
@@ -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<NotKeyed, InMemoryState, DefaultClock> {
|
|
||||||
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<RateLimiter<NotKeyed, InMemoryState, DefaultClock>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<E: Endpoint> Middleware<E> for RateLimit {
|
|
||||||
type Output = RateLimitEndpoint<E>;
|
|
||||||
|
|
||||||
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<E> {
|
|
||||||
endpoint: E,
|
|
||||||
limiter: Arc<RateLimiter<NotKeyed, InMemoryState, DefaultClock>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<E: Endpoint> Endpoint for RateLimitEndpoint<E> {
|
|
||||||
type Output = Response;
|
|
||||||
|
|
||||||
async fn call(&self, req: Request) -> Result<Self::Output> {
|
|
||||||
// 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<E> RateLimitEndpoint<E> {
|
|
||||||
/// Extracts the client IP address from the request.
|
|
||||||
fn get_client_ip(req: &Request) -> Option<IpAddr> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Self, Self::Error> {
|
|
||||||
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<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Object, serde::Deserialize)]
|
|
||||||
struct ContactResponse {
|
|
||||||
success: bool,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ContactResponse> for Json<ContactResponse> {
|
|
||||||
fn from(value: ContactResponse) -> Self {
|
|
||||||
Self(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(ApiResponse)]
|
|
||||||
enum ContactApiResponse {
|
|
||||||
/// Success
|
|
||||||
#[oai(status = 200)]
|
|
||||||
Ok(Json<ContactResponse>),
|
|
||||||
/// Bad Request - validation failed
|
|
||||||
#[oai(status = 400)]
|
|
||||||
BadRequest(Json<ContactResponse>),
|
|
||||||
/// Too Many Requests - rate limit exceeded
|
|
||||||
#[oai(status = 429)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
TooManyRequests,
|
|
||||||
/// Internal Server Error
|
|
||||||
#[oai(status = 500)]
|
|
||||||
InternalServerError(Json<ContactResponse>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// API for handling contact form submissions and sending emails.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ContactApi {
|
|
||||||
settings: EmailSettings,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<EmailSettings> 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<ContactRequest>,
|
|
||||||
remote_addr: Option<poem::web::Data<&poem::web::RemoteAddr>>,
|
|
||||||
) -> 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<dyn std::error::Error>> {
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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<Meta>),
|
|
||||||
/// 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<MetaResponse> {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Self, config::ConfigError> {
|
|
||||||
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<String> for Environment {
|
|
||||||
type Error = String;
|
|
||||||
|
|
||||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
||||||
Self::try_from(value.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for Environment {
|
|
||||||
type Error = String;
|
|
||||||
|
|
||||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
|
||||||
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<Self, Self::Error> {
|
|
||||||
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<String> for Starttls {
|
|
||||||
type Error = String;
|
|
||||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
|
||||||
value.as_str().try_into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<bool> 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<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
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<E>(self, value: &str) -> Result<Starttls, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
Starttls::try_from(value).map_err(E::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_string<E>(self, value: String) -> Result<Starttls, E>
|
|
||||||
where
|
|
||||||
E: serde::de::Error,
|
|
||||||
{
|
|
||||||
Starttls::try_from(value.as_str()).map_err(E::custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_bool<E>(self, value: bool) -> Result<Starttls, E>
|
|
||||||
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<Starttls, _> = 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<Starttls, _> = 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<Starttls, _> = 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<Starttls, _> = 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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<poem::listener::TcpListener<String>, std::convert::Infallible>;
|
|
||||||
/// The configured application with rate limiting, CORS, and settings data.
|
|
||||||
pub type App = AddDataEndpoint<CorsEndpoint<RateLimitEndpoint<Route>>, 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<RunnableApplication> for App {
|
|
||||||
fn from(value: RunnableApplication) -> Self {
|
|
||||||
value.app
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Application> 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<poem::listener::TcpListener<String>>,
|
|
||||||
) -> 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<poem::listener::TcpListener<String>>,
|
|
||||||
) -> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
49
flake.lock
generated
49
flake.lock
generated
@@ -68,11 +68,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1761922975,
|
"lastModified": 1763136231,
|
||||||
"narHash": "sha256-j4EB5ku/gDm7h7W7A+k70RYj5nUiW/l9wQtXMJUD2hg=",
|
"narHash": "sha256-QVtIjPSQ/xVhuXSSENYOYZPfrjjc/W/djuxcJyKxGTw=",
|
||||||
"owner": "cachix",
|
"owner": "cachix",
|
||||||
"repo": "devenv",
|
"repo": "devenv",
|
||||||
"rev": "c9f0b47815a4895fadac87812de8a4de27e0ace1",
|
"rev": "4b8c2bbdb4e01ef8c4093ee1224fe21ed5ea1a5e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -140,6 +140,24 @@
|
|||||||
"type": "github"
|
"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": {
|
"flakeCompat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
@@ -264,9 +282,8 @@
|
|||||||
"inputs": {
|
"inputs": {
|
||||||
"alejandra": "alejandra",
|
"alejandra": "alejandra",
|
||||||
"devenv": "devenv",
|
"devenv": "devenv",
|
||||||
"nixpkgs": "nixpkgs",
|
"flake-utils": "flake-utils",
|
||||||
"rust-overlay": "rust-overlay",
|
"nixpkgs": "nixpkgs"
|
||||||
"systems": "systems"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
@@ -286,26 +303,6 @@
|
|||||||
"type": "github"
|
"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": {
|
"systems": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681028828,
|
"lastModified": 1681028828,
|
||||||
|
|||||||
25
flake.nix
25
flake.nix
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
|
nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
|
||||||
systems.url = "github:nix-systems/default";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
alejandra = {
|
alejandra = {
|
||||||
url = "github:kamadorueda/alejandra/4.0.0";
|
url = "github:kamadorueda/alejandra/4.0.0";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
@@ -10,10 +10,6 @@
|
|||||||
url = "github:cachix/devenv";
|
url = "github:cachix/devenv";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
rust-overlay = {
|
|
||||||
url = "github:oxalica/rust-overlay";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nixConfig = {
|
nixConfig = {
|
||||||
@@ -30,27 +26,18 @@
|
|||||||
outputs = {
|
outputs = {
|
||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
devenv,
|
|
||||||
systems,
|
|
||||||
rust-overlay,
|
|
||||||
alejandra,
|
alejandra,
|
||||||
|
flake-utils,
|
||||||
...
|
...
|
||||||
} @ inputs: let
|
} @ inputs:
|
||||||
forEachSystem = nixpkgs.lib.genAttrs (import systems);
|
flake-utils.lib.eachDefaultSystem (
|
||||||
in {
|
|
||||||
formatter = forEachSystem (system: alejandra.defaultPackage.${system});
|
|
||||||
packages = forEachSystem (system: import ./backend/nix/package.nix { inherit rust-overlay inputs system; });
|
|
||||||
devShells = forEachSystem (
|
|
||||||
system: let
|
system: let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in {
|
in {
|
||||||
backend = import ./backend/nix/shell.nix {
|
formatter = alejandra.defaultPackage.${system};
|
||||||
inherit inputs pkgs system self rust-overlay;
|
devShell = import ./nix/shell.nix {
|
||||||
};
|
|
||||||
frontend = import ./frontend/shell.nix {
|
|
||||||
inherit inputs pkgs self;
|
inherit inputs pkgs self;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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"
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
33
nix/shell.nix
Normal file
33
nix/shell.nix
Normal file
@@ -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"
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -32,26 +32,23 @@
|
|||||||
"nitropack": "^2.12.9",
|
"nitropack": "^2.12.9",
|
||||||
"nuxi": "^3.30.0",
|
"nuxi": "^3.30.0",
|
||||||
"nuxt": "^4.2.0",
|
"nuxt": "^4.2.0",
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"vite": "^7.1.12",
|
"vite": "^7.1.12",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.73",
|
||||||
"@iconify-json/material-symbols": "^1.2.44",
|
"@iconify-json/material-symbols": "^1.2.44",
|
||||||
"@iconify-json/material-symbols-light": "^1.2.44",
|
"@iconify-json/material-symbols-light": "^1.2.44",
|
||||||
"@iconify-json/mdi": "^1.2.3",
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
|
"@iconify-json/simple-icons": "^1.2.58",
|
||||||
"@nuxtjs/i18n": "^10.2.0",
|
"@nuxtjs/i18n": "^10.2.0",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
"less": "^4.4.2",
|
"less": "^4.4.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
|
||||||
"pnpm": {
|
|
||||||
"overrides": {
|
|
||||||
"sharp": "0.33.4"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
505
frontend/pnpm-lock.yaml → pnpm-lock.yaml
generated
505
frontend/pnpm-lock.yaml → pnpm-lock.yaml
generated
@@ -4,9 +4,6 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
overrides:
|
|
||||||
sharp: 0.33.4
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -61,10 +58,7 @@ importers:
|
|||||||
version: 3.30.0
|
version: 3.30.0
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^4.2.0
|
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)
|
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)
|
||||||
typescript:
|
|
||||||
specifier: ^5.9.3
|
|
||||||
version: 5.9.3
|
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.1.12
|
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)
|
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
|
specifier: ^4.6.3
|
||||||
version: 4.6.3(vue@3.5.24(typescript@5.9.3))
|
version: 4.6.3(vue@3.5.24(typescript@5.9.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@iconify-json/lucide':
|
||||||
|
specifier: ^1.2.73
|
||||||
|
version: 1.2.73
|
||||||
'@iconify-json/material-symbols':
|
'@iconify-json/material-symbols':
|
||||||
specifier: ^1.2.44
|
specifier: ^1.2.44
|
||||||
version: 1.2.45
|
version: 1.2.46
|
||||||
'@iconify-json/material-symbols-light':
|
'@iconify-json/material-symbols-light':
|
||||||
specifier: ^1.2.44
|
specifier: ^1.2.44
|
||||||
version: 1.2.45
|
version: 1.2.46
|
||||||
'@iconify-json/mdi':
|
'@iconify-json/mdi':
|
||||||
specifier: ^1.2.3
|
specifier: ^1.2.3
|
||||||
version: 1.2.3
|
version: 1.2.3
|
||||||
|
'@iconify-json/simple-icons':
|
||||||
|
specifier: ^1.2.58
|
||||||
|
version: 1.2.58
|
||||||
'@nuxtjs/i18n':
|
'@nuxtjs/i18n':
|
||||||
specifier: ^10.2.0
|
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))
|
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:
|
tailwindcss:
|
||||||
specifier: ^4.1.17
|
specifier: ^4.1.17
|
||||||
version: 4.1.17
|
version: 4.1.17
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.9.3
|
||||||
|
version: 5.9.3
|
||||||
zod:
|
zod:
|
||||||
specifier: ^4.1.12
|
specifier: ^4.1.12
|
||||||
version: 4.1.12
|
version: 4.1.12
|
||||||
@@ -299,11 +302,11 @@ packages:
|
|||||||
'@dxup/unimport@0.1.2':
|
'@dxup/unimport@0.1.2':
|
||||||
resolution: {integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==}
|
resolution: {integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==}
|
||||||
|
|
||||||
'@emnapi/core@1.7.0':
|
'@emnapi/core@1.7.1':
|
||||||
resolution: {integrity: sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==}
|
resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==}
|
||||||
|
|
||||||
'@emnapi/runtime@1.7.0':
|
'@emnapi/runtime@1.7.1':
|
||||||
resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==}
|
resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==}
|
||||||
|
|
||||||
'@emnapi/wasi-threads@1.1.0':
|
'@emnapi/wasi-threads@1.1.0':
|
||||||
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
|
resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==}
|
||||||
@@ -560,17 +563,23 @@ packages:
|
|||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
'@iconify-json/material-symbols-light@1.2.45':
|
'@iconify-json/lucide@1.2.73':
|
||||||
resolution: {integrity: sha512-FtS0UouUSo6cdMZyhp9cDaVWxg14KTV9YjDnt/Dm7LRi59sO+kvKdPOiXZk57kIhE4MUE8qWClNvn+fi5a0pMA==}
|
resolution: {integrity: sha512-++HFkqDNu4jqG5+vYT+OcVj9OiuPCw9wQuh8G5QWQnBRSJ9eKwSStiU8ORgOoK07xJsm/0VIHySMniXUUXP9Gw==}
|
||||||
|
|
||||||
'@iconify-json/material-symbols@1.2.45':
|
'@iconify-json/material-symbols-light@1.2.46':
|
||||||
resolution: {integrity: sha512-QGt+57HpuYNYHIjmGRyaCTSStJD4AOj4C6xKFoMd1gtYPzfgK4/MOl3ar3WsgQnAmGo1QuDFOztXIIjDgDO2Kg==}
|
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':
|
'@iconify-json/mdi@1.2.3':
|
||||||
resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==}
|
resolution: {integrity: sha512-O3cLwbDOK7NNDf2ihaQOH5F9JglnulNDFV7WprU2dSoZu3h3cWH//h74uQAB87brHmvFVxIOkuBX2sZSzYhScg==}
|
||||||
|
|
||||||
'@iconify/collections@1.0.617':
|
'@iconify-json/simple-icons@1.2.58':
|
||||||
resolution: {integrity: sha512-coRyOUT2gQ8SHptjaW5YG2VEscoIcpEAN/mn1UgorKKgoA6l+aoebW58WXLWcvSZ4LKEmim3Hnp/bLMLMoOejg==}
|
resolution: {integrity: sha512-XtXEoRALqztdNc9ujYBj2tTCPKdIPKJBdLNDebFF46VV1aOAwTbAYMgNsK5GMCpTJupLCmpBWDn+gX5SpECorQ==}
|
||||||
|
|
||||||
|
'@iconify/collections@1.0.618':
|
||||||
|
resolution: {integrity: sha512-G0pOVenguqtHC3mV07JFqNHJnZl0Mq+Jc8Uksx6tVrq4KktenM5s9yih83FK7PCCCEHccvS/SkD06cfZOvd0Yw==}
|
||||||
|
|
||||||
'@iconify/types@2.0.0':
|
'@iconify/types@2.0.0':
|
||||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||||
@@ -583,119 +592,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: '>=3'
|
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':
|
'@internationalized/date@3.10.0':
|
||||||
resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==}
|
resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==}
|
||||||
|
|
||||||
@@ -2547,6 +2443,36 @@ packages:
|
|||||||
bare-abort-controller:
|
bare-abort-controller:
|
||||||
optional: true
|
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:
|
base64-js@1.5.1:
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
|
|
||||||
@@ -2868,8 +2794,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
|
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
|
||||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
|
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
|
||||||
|
|
||||||
csstype@3.1.3:
|
csstype@3.2.0:
|
||||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
resolution: {integrity: sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==}
|
||||||
|
|
||||||
db0@0.3.4:
|
db0@0.3.4:
|
||||||
resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==}
|
resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==}
|
||||||
@@ -2930,8 +2856,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
default-browser-id@5.0.0:
|
default-browser-id@5.0.1:
|
||||||
resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
|
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
default-browser@5.3.0:
|
default-browser@5.3.0:
|
||||||
@@ -3023,8 +2949,8 @@ packages:
|
|||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.250:
|
electron-to-chromium@1.5.252:
|
||||||
resolution: {integrity: sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==}
|
resolution: {integrity: sha512-53uTpjtRgS7gjIxZ4qCgFdNO2q+wJt/Z8+xAvxbCqXPJrY6h7ighUkadQmNMXH96crtpa6gPFNP7BF4UBGDuaA==}
|
||||||
|
|
||||||
embla-carousel-auto-height@8.6.0:
|
embla-carousel-auto-height@8.6.0:
|
||||||
resolution: {integrity: sha512-/HrJQOEM6aol/oF33gd2QlINcXy3e19fJWvHDuHWp2bpyTa+2dm9tVVJak30m2Qy6QyQ6Fc8DkImtv7pxWOJUQ==}
|
resolution: {integrity: sha512-/HrJQOEM6aol/oF33gd2QlINcXy3e19fJWvHDuHWp2bpyTa+2dm9tVVJak30m2Qy6QyQ6Fc8DkImtv7pxWOJUQ==}
|
||||||
@@ -3900,8 +3826,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
|
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
knitwork@1.2.0:
|
knitwork@1.3.0:
|
||||||
resolution: {integrity: sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==}
|
resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==}
|
||||||
|
|
||||||
kolorist@1.8.0:
|
kolorist@1.8.0:
|
||||||
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
||||||
@@ -4344,10 +4270,13 @@ packages:
|
|||||||
xml2js:
|
xml2js:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
node-abi@3.83.0:
|
node-abi@3.85.0:
|
||||||
resolution: {integrity: sha512-o2PH88PgFlfoSDjU5oq/b/p9m+DJaPfslRI5FzNqcK1ea1i2/8xo/FL850kdgw0EAQJ/cSyyi2W2fBjHBdg5rA==}
|
resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
node-addon-api@6.1.0:
|
||||||
|
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
|
||||||
|
|
||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
@@ -5082,9 +5011,9 @@ packages:
|
|||||||
setprototypeof@1.2.0:
|
setprototypeof@1.2.0:
|
||||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||||
|
|
||||||
sharp@0.33.4:
|
sharp@0.32.6:
|
||||||
resolution: {integrity: sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==}
|
resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==}
|
||||||
engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: '>=14.15.0'}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
@@ -5322,6 +5251,9 @@ packages:
|
|||||||
tar-fs@2.1.4:
|
tar-fs@2.1.4:
|
||||||
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
||||||
|
|
||||||
|
tar-fs@3.1.1:
|
||||||
|
resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==}
|
||||||
|
|
||||||
tar-stream@2.2.0:
|
tar-stream@2.2.0:
|
||||||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -5518,8 +5450,8 @@ packages:
|
|||||||
'@nuxt/kit':
|
'@nuxt/kit':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
unplugin-vue-router@0.16.1:
|
unplugin-vue-router@0.16.2:
|
||||||
resolution: {integrity: sha512-7A7gUVzLIYMBrBPKk8l4lZoZXDOrO8+etw6/RTrqG3OzpLUUZEXJFUW7+OyMIpQK93sEbdkR2z9ZNNl/r32FMw==}
|
resolution: {integrity: sha512-lE6ZjnHaXfS2vFI/PSEwdKcdOo5RwAbCKUnPBIN9YwLgSWas3x+qivzQvJa/uxhKzJldE6WK43aDKjGj9Rij9w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@vue/compiler-sfc': ^3.5.17
|
'@vue/compiler-sfc': ^3.5.17
|
||||||
vue-router: ^4.6.0
|
vue-router: ^4.6.0
|
||||||
@@ -5658,8 +5590,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0
|
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:
|
vite-node@5.1.0:
|
||||||
resolution: {integrity: sha512-nJINVH7lHBKoyDFYnwrXbNUrmTJ2ssBHTd/mXVZfLq/O5K7ksv4CayQOA5KkbOSrsgSQg8antcVPgQmzBWWn/w==}
|
resolution: {integrity: sha512-ci+CXFFrQfRgdO0WDSKNQ28OOglURJUw2hVlfir4IA+Q2nHKmU/qIbmiYO7oB8CZvvSoyCmHycXz5MiX03BrsQ==}
|
||||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -5804,6 +5736,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.5.0
|
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:
|
vue@3.5.24:
|
||||||
resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==}
|
resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6211,13 +6149,13 @@ snapshots:
|
|||||||
|
|
||||||
'@dxup/unimport@0.1.2': {}
|
'@dxup/unimport@0.1.2': {}
|
||||||
|
|
||||||
'@emnapi/core@1.7.0':
|
'@emnapi/core@1.7.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.1.0
|
'@emnapi/wasi-threads': 1.1.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@emnapi/runtime@1.7.0':
|
'@emnapi/runtime@1.7.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
@@ -6430,11 +6368,15 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
'@iconify-json/material-symbols-light@1.2.45':
|
'@iconify-json/lucide@1.2.73':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@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:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
@@ -6442,7 +6384,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@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:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
@@ -6466,81 +6412,6 @@ snapshots:
|
|||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
vue: 3.5.24(typescript@5.9.3)
|
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':
|
'@internationalized/date@3.10.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@swc/helpers': 0.5.17
|
'@swc/helpers': 0.5.17
|
||||||
@@ -6696,15 +6567,15 @@ snapshots:
|
|||||||
|
|
||||||
'@napi-rs/wasm-runtime@0.2.12':
|
'@napi-rs/wasm-runtime@0.2.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.7.0
|
'@emnapi/core': 1.7.1
|
||||||
'@emnapi/runtime': 1.7.0
|
'@emnapi/runtime': 1.7.1
|
||||||
'@tybys/wasm-util': 0.10.1
|
'@tybys/wasm-util': 0.10.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.0.7':
|
'@napi-rs/wasm-runtime@1.0.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/core': 1.7.0
|
'@emnapi/core': 1.7.1
|
||||||
'@emnapi/runtime': 1.7.0
|
'@emnapi/runtime': 1.7.1
|
||||||
'@tybys/wasm-util': 0.10.1
|
'@tybys/wasm-util': 0.10.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -6779,7 +6650,7 @@ snapshots:
|
|||||||
hookable: 5.5.3
|
hookable: 5.5.3
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
json-schema-to-typescript: 15.0.4
|
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-hast: 13.2.0
|
||||||
mdast-util-to-string: 4.0.0
|
mdast-util-to-string: 4.0.0
|
||||||
micromark: 4.0.2
|
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))':
|
'@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:
|
dependencies:
|
||||||
'@iconify/collections': 1.0.617
|
'@iconify/collections': 1.0.618
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
'@iconify/utils': 3.0.2
|
'@iconify/utils': 3.0.2
|
||||||
'@iconify/vue': 5.0.0(vue@3.5.24(typescript@5.9.3))
|
'@iconify/vue': 5.0.0(vue@3.5.24(typescript@5.9.3))
|
||||||
@@ -7081,7 +6952,7 @@ snapshots:
|
|||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
h3: 1.15.4
|
h3: 1.15.4
|
||||||
image-meta: 0.2.2
|
image-meta: 0.2.2
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
@@ -7104,10 +6975,13 @@ snapshots:
|
|||||||
- '@vercel/functions'
|
- '@vercel/functions'
|
||||||
- '@vercel/kv'
|
- '@vercel/kv'
|
||||||
- aws4fetch
|
- aws4fetch
|
||||||
|
- bare-abort-controller
|
||||||
|
- bare-buffer
|
||||||
- db0
|
- db0
|
||||||
- idb-keyval
|
- idb-keyval
|
||||||
- ioredis
|
- ioredis
|
||||||
- magicast
|
- magicast
|
||||||
|
- react-native-b4a
|
||||||
- uploadthing
|
- uploadthing
|
||||||
|
|
||||||
'@nuxt/kit@3.20.1(magicast@0.5.1)':
|
'@nuxt/kit@3.20.1(magicast@0.5.1)':
|
||||||
@@ -7121,7 +6995,7 @@ snapshots:
|
|||||||
ignore: 7.0.5
|
ignore: 7.0.5
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
klona: 2.0.6
|
klona: 2.0.6
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
@@ -7161,7 +7035,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- 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:
|
dependencies:
|
||||||
'@nuxt/devalue': 2.0.2
|
'@nuxt/devalue': 2.0.2
|
||||||
'@nuxt/kit': 4.2.1(magicast@0.5.1)
|
'@nuxt/kit': 4.2.1(magicast@0.5.1)
|
||||||
@@ -7179,7 +7053,7 @@ snapshots:
|
|||||||
klona: 2.0.6
|
klona: 2.0.6
|
||||||
mocked-exports: 0.1.1
|
mocked-exports: 0.1.1
|
||||||
nitropack: 2.12.9(better-sqlite3@12.4.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
|
pathe: 2.0.3
|
||||||
pkg-types: 2.3.0
|
pkg-types: 2.3.0
|
||||||
radix3: 1.1.2
|
radix3: 1.1.2
|
||||||
@@ -7355,7 +7229,7 @@ snapshots:
|
|||||||
embla-carousel-wheel-gestures: 8.1.0(embla-carousel@8.6.0)
|
embla-carousel-wheel-gestures: 8.1.0(embla-carousel@8.6.0)
|
||||||
fuse.js: 7.1.0
|
fuse.js: 7.1.0
|
||||||
hookable: 5.5.3
|
hookable: 5.5.3
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.0
|
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))
|
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
|
- vite
|
||||||
- vue
|
- 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:
|
dependencies:
|
||||||
'@nuxt/kit': 4.2.1(magicast@0.5.1)
|
'@nuxt/kit': 4.2.1(magicast@0.5.1)
|
||||||
'@rollup/plugin-replace': 6.0.3(rollup@4.53.2)
|
'@rollup/plugin-replace': 6.0.3(rollup@4.53.2)
|
||||||
@@ -7435,11 +7309,11 @@ snapshots:
|
|||||||
get-port-please: 3.2.0
|
get-port-please: 3.2.0
|
||||||
h3: 1.15.4
|
h3: 1.15.4
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
mocked-exports: 0.1.1
|
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
|
pathe: 2.0.3
|
||||||
pkg-types: 2.3.0
|
pkg-types: 2.3.0
|
||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
@@ -7449,8 +7323,8 @@ snapshots:
|
|||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
unenv: 2.0.0-rc.24
|
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: 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)
|
||||||
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))
|
||||||
vue: 3.5.24(typescript@5.9.3)
|
vue: 3.5.24(typescript@5.9.3)
|
||||||
vue-bundle-renderer: 2.2.0
|
vue-bundle-renderer: 2.2.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -7505,7 +7379,7 @@ snapshots:
|
|||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
devalue: 5.5.0
|
devalue: 5.5.0
|
||||||
h3: 1.15.4
|
h3: 1.15.4
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
nuxt-define: 1.0.0
|
nuxt-define: 1.0.0
|
||||||
@@ -7517,7 +7391,7 @@ snapshots:
|
|||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
unplugin: 2.3.10
|
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)
|
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-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))
|
vue-router: 4.6.3(vue@3.5.24(typescript@5.9.3))
|
||||||
@@ -8625,7 +8499,7 @@ snapshots:
|
|||||||
'@vue/reactivity': 3.5.24
|
'@vue/reactivity': 3.5.24
|
||||||
'@vue/runtime-core': 3.5.24
|
'@vue/runtime-core': 3.5.24
|
||||||
'@vue/shared': 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))':
|
'@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.9.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8810,6 +8684,41 @@ snapshots:
|
|||||||
|
|
||||||
bare-events@2.8.2: {}
|
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: {}
|
base64-js@1.5.1: {}
|
||||||
|
|
||||||
baseline-browser-mapping@2.8.28: {}
|
baseline-browser-mapping@2.8.28: {}
|
||||||
@@ -8856,7 +8765,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.8.28
|
baseline-browser-mapping: 2.8.28
|
||||||
caniuse-lite: 1.0.30001754
|
caniuse-lite: 1.0.30001754
|
||||||
electron-to-chromium: 1.5.250
|
electron-to-chromium: 1.5.252
|
||||||
node-releases: 2.0.27
|
node-releases: 2.0.27
|
||||||
update-browserslist-db: 1.1.4(browserslist@4.28.0)
|
update-browserslist-db: 1.1.4(browserslist@4.28.0)
|
||||||
|
|
||||||
@@ -9153,7 +9062,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
css-tree: 2.2.1
|
css-tree: 2.2.1
|
||||||
|
|
||||||
csstype@3.1.3: {}
|
csstype@3.2.0: {}
|
||||||
|
|
||||||
db0@0.3.4(better-sqlite3@12.4.1):
|
db0@0.3.4(better-sqlite3@12.4.1):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -9181,12 +9090,12 @@ snapshots:
|
|||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
default-browser-id@5.0.0: {}
|
default-browser-id@5.0.1: {}
|
||||||
|
|
||||||
default-browser@5.3.0:
|
default-browser@5.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bundle-name: 4.1.0
|
bundle-name: 4.1.0
|
||||||
default-browser-id: 5.0.0
|
default-browser-id: 5.0.1
|
||||||
|
|
||||||
define-lazy-prop@2.0.0: {}
|
define-lazy-prop@2.0.0: {}
|
||||||
|
|
||||||
@@ -9250,7 +9159,7 @@ snapshots:
|
|||||||
|
|
||||||
ee-first@1.1.1: {}
|
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):
|
embla-carousel-auto-height@8.6.0(embla-carousel@8.6.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -10124,7 +10033,7 @@ snapshots:
|
|||||||
listhen: 1.9.0
|
listhen: 1.9.0
|
||||||
ofetch: 1.5.1
|
ofetch: 1.5.1
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
sharp: 0.33.4
|
sharp: 0.32.6
|
||||||
svgo: 3.3.2
|
svgo: 3.3.2
|
||||||
ufo: 1.6.1
|
ufo: 1.6.1
|
||||||
unstorage: 1.17.2(db0@0.3.4(better-sqlite3@12.4.1))(ioredis@5.8.2)
|
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/functions'
|
||||||
- '@vercel/kv'
|
- '@vercel/kv'
|
||||||
- aws4fetch
|
- aws4fetch
|
||||||
|
- bare-abort-controller
|
||||||
|
- bare-buffer
|
||||||
- db0
|
- db0
|
||||||
- idb-keyval
|
- idb-keyval
|
||||||
- ioredis
|
- ioredis
|
||||||
|
- react-native-b4a
|
||||||
- uploadthing
|
- uploadthing
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -10310,7 +10222,7 @@ snapshots:
|
|||||||
|
|
||||||
klona@2.0.6: {}
|
klona@2.0.6: {}
|
||||||
|
|
||||||
knitwork@1.2.0: {}
|
knitwork@1.3.0: {}
|
||||||
|
|
||||||
kolorist@1.8.0: {}
|
kolorist@1.8.0: {}
|
||||||
|
|
||||||
@@ -10944,7 +10856,7 @@ snapshots:
|
|||||||
ioredis: 5.8.2
|
ioredis: 5.8.2
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
klona: 2.0.6
|
klona: 2.0.6
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
listhen: 1.9.0
|
listhen: 1.9.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
magicast: 0.5.1
|
magicast: 0.5.1
|
||||||
@@ -11009,10 +10921,13 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- uploadthing
|
- uploadthing
|
||||||
|
|
||||||
node-abi@3.83.0:
|
node-abi@3.85.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
|
|
||||||
|
node-addon-api@6.1.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-addon-api@7.1.1: {}
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
node-emoji@2.2.0:
|
node-emoji@2.2.0:
|
||||||
@@ -11075,16 +10990,16 @@ snapshots:
|
|||||||
|
|
||||||
nuxt-define@1.0.0: {}
|
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:
|
dependencies:
|
||||||
'@dxup/nuxt': 0.2.2(magicast@0.5.1)
|
'@dxup/nuxt': 0.2.2(magicast@0.5.1)
|
||||||
'@nuxt/cli': 3.30.0(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/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/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/schema': 4.2.1
|
||||||
'@nuxt/telemetry': 2.6.6(magicast@0.5.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))
|
'@unhead/vue': 2.0.19(vue@3.5.24(typescript@5.9.3))
|
||||||
'@vue/shared': 3.5.24
|
'@vue/shared': 3.5.24
|
||||||
c12: 3.3.2(magicast@0.5.1)
|
c12: 3.3.2(magicast@0.5.1)
|
||||||
@@ -11104,7 +11019,7 @@ snapshots:
|
|||||||
impound: 1.0.0
|
impound: 1.0.0
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
klona: 2.0.6
|
klona: 2.0.6
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
nanotar: 0.2.0
|
nanotar: 0.2.0
|
||||||
@@ -11130,7 +11045,7 @@ snapshots:
|
|||||||
unctx: 2.4.1
|
unctx: 2.4.1
|
||||||
unimport: 5.5.0
|
unimport: 5.5.0
|
||||||
unplugin: 2.3.10
|
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
|
untyped: 2.0.0
|
||||||
vue: 3.5.24(typescript@5.9.3)
|
vue: 3.5.24(typescript@5.9.3)
|
||||||
vue-router: 4.6.3(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
|
minimist: 1.2.8
|
||||||
mkdirp-classic: 0.5.3
|
mkdirp-classic: 0.5.3
|
||||||
napi-build-utils: 2.0.0
|
napi-build-utils: 2.0.0
|
||||||
node-abi: 3.83.0
|
node-abi: 3.85.0
|
||||||
pump: 3.0.3
|
pump: 3.0.3
|
||||||
rc: 1.2.8
|
rc: 1.2.8
|
||||||
simple-get: 4.0.1
|
simple-get: 4.0.1
|
||||||
@@ -12024,31 +11939,20 @@ snapshots:
|
|||||||
|
|
||||||
setprototypeof@1.2.0: {}
|
setprototypeof@1.2.0: {}
|
||||||
|
|
||||||
sharp@0.33.4:
|
sharp@0.32.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
color: 4.2.3
|
color: 4.2.3
|
||||||
detect-libc: 2.1.2
|
detect-libc: 2.1.2
|
||||||
|
node-addon-api: 6.1.0
|
||||||
|
prebuild-install: 7.1.3
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
optionalDependencies:
|
simple-get: 4.0.1
|
||||||
'@img/sharp-darwin-arm64': 0.33.4
|
tar-fs: 3.1.1
|
||||||
'@img/sharp-darwin-x64': 0.33.4
|
tunnel-agent: 0.6.0
|
||||||
'@img/sharp-libvips-darwin-arm64': 1.0.2
|
transitivePeerDependencies:
|
||||||
'@img/sharp-libvips-darwin-x64': 1.0.2
|
- bare-abort-controller
|
||||||
'@img/sharp-libvips-linux-arm': 1.0.2
|
- bare-buffer
|
||||||
'@img/sharp-libvips-linux-arm64': 1.0.2
|
- react-native-b4a
|
||||||
'@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
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
@@ -12291,6 +12195,19 @@ snapshots:
|
|||||||
pump: 3.0.3
|
pump: 3.0.3
|
||||||
tar-stream: 2.2.0
|
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:
|
tar-stream@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bl: 4.1.0
|
bl: 4.1.0
|
||||||
@@ -12531,7 +12448,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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:
|
dependencies:
|
||||||
'@babel/generator': 7.28.5
|
'@babel/generator': 7.28.5
|
||||||
'@vue-macros/common': 3.1.1(vue@3.5.24(typescript@5.9.3))
|
'@vue-macros/common': 3.1.1(vue@3.5.24(typescript@5.9.3))
|
||||||
@@ -12613,12 +12530,12 @@ snapshots:
|
|||||||
citty: 0.1.6
|
citty: 0.1.6
|
||||||
defu: 6.1.4
|
defu: 6.1.4
|
||||||
jiti: 2.6.1
|
jiti: 2.6.1
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
scule: 1.3.0
|
scule: 1.3.0
|
||||||
|
|
||||||
unwasm@0.3.11:
|
unwasm@0.3.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
@@ -12628,7 +12545,7 @@ snapshots:
|
|||||||
unwasm@0.5.0:
|
unwasm@0.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
exsolve: 1.0.8
|
exsolve: 1.0.8
|
||||||
knitwork: 1.2.0
|
knitwork: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
@@ -12685,7 +12602,7 @@ snapshots:
|
|||||||
dependencies:
|
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: 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:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -12706,7 +12623,7 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- 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:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
chokidar: 4.0.3
|
chokidar: 4.0.3
|
||||||
@@ -12721,6 +12638,7 @@ snapshots:
|
|||||||
eslint: 9.39.1(jiti@2.6.1)
|
eslint: 9.39.1(jiti@2.6.1)
|
||||||
optionator: 0.9.4
|
optionator: 0.9.4
|
||||||
typescript: 5.9.3
|
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)):
|
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:
|
dependencies:
|
||||||
@@ -12828,6 +12746,13 @@ snapshots:
|
|||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
vue: 3.5.24(typescript@5.9.3)
|
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):
|
vue@3.5.24(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/compiler-dom': 3.5.24
|
'@vue/compiler-dom': 3.5.24
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -1,6 +1,13 @@
|
|||||||
{
|
{
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
"files": [],
|
"files": [],
|
||||||
|
"exclude": [
|
||||||
|
".jj",
|
||||||
|
".git",
|
||||||
|
"**/.output/*",
|
||||||
|
"**/node_modules/*",
|
||||||
|
"**/dist/*"
|
||||||
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "./.nuxt/tsconfig.app.json"
|
"path": "./.nuxt/tsconfig.app.json"
|
||||||
Reference in New Issue
Block a user