commit eaff977b2257266272e2f9e170576e2c4222fa6e Author: Elias Schneider Date: Mon Aug 12 11:00:25 2024 +0200 initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..f178de6e --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +PUBLIC_APP_URL=http://localhost diff --git a/.env.test b/.env.test new file mode 100644 index 00000000..dad03cc6 --- /dev/null +++ b/.env.test @@ -0,0 +1,2 @@ +APP_ENV=test +PUBLIC_APP_URL=http://localhost \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..da4b14c4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# These are supported funding model platforms +github: stonith404 diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 00000000..78cf08e1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,37 @@ +name: "🐛 Bug Report" +description: "Report something that is not working as expected" +title: "🐛 Bug Report: " +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out our bug report form 🙏 + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: "Reproduction steps" + description: "How do you trigger this bug? Please walk us through it step by step." + placeholder: "When I ..." + - type: textarea + id: expected-behavior + validations: + required: true + attributes: + label: "Expected behavior" + description: "What did you think would happen?" + placeholder: "It should ..." + - type: textarea + id: actual-behavior + validations: + required: true + attributes: + label: "Actual Behavior" + description: "What did actually happen? Add screenshots, if applicable." + placeholder: "It actually ..." + - type: markdown + attributes: + value: | + Before submitting, please check if the issues hasn't been raised before. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..ec4bb386 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 00000000..9d8f8c29 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,25 @@ +name: 🚀 Feature +description: "Submit a proposal for a new feature" +title: "🚀 Feature: " +labels: [feature] +body: + - type: textarea + id: feature-description + validations: + required: true + attributes: + label: "Feature description" + description: "A clear and concise description of what the feature is." + placeholder: "You should add ..." + - type: textarea + id: pitch + validations: + required: true + attributes: + label: "Pitch" + description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable." + placeholder: "In my use-case, ..." + - type: markdown + attributes: + value: | + Before submitting, please check if the feature hasn't been proposed before. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 00000000..ea70913b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,17 @@ +name: ❓ Question +description: "Ask a question +title: "❓ Question:" +labels: [question] +body: + - type: textarea + id: feature-description + validations: + required: true + attributes: + label: "🙋Question" + description: "A clear question. Please provide as much detail as possible." + placeholder: "How do I ...?" + - type: markdown + attributes: + value: | + Before submitting, please check if the question hasn't been asked before. \ No newline at end of file diff --git a/.github/workflows/build-and-push-docker-image.yml b/.github/workflows/build-and-push-docker-image.yml new file mode 100644 index 00000000..0649d747 --- /dev/null +++ b/.github/workflows/build-and-push-docker-image.yml @@ -0,0 +1,34 @@ +name: Build and Push Docker Image + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: checkout code + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker registry + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} + password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: stonith404/pocket-id:latest,stonith404/pocket-id:${{ github.ref_name }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 00000000..032aa5cf --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,45 @@ +name: E2E Tests +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + build-and-test: + timeout-minutes: 20 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Build Docker Image + run: docker build -t stonith404/pocket-id . + - name: Run Docker Container + run: docker run -d --name pocket-id -p 80:80 --env-file .env.test stonith404/pocket-id + + - name: Install frontend dependencies + working-directory: ./frontend + run: npm ci + + - name: Install Playwright Browsers + working-directory: ./frontend + run: npx playwright install --with-deps chromium + + - name: Run Playwright tests + working-directory: ./frontend + run: npx playwright test + + - name: Get container logs + if: always() + run: docker logs pocket-id + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: frontend/tests/.output + retention-days: 15 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..fe485c89 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# JetBrains +**/.idea + +node_modules + +# Output +.output +.vercel +/frontend/.svelte-kit +/frontend/build +/backend/bin + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Application specific +data +/frontend/tests/.auth +pocket-id-backend \ No newline at end of file diff --git a/.version b/.version new file mode 100644 index 00000000..bd52db81 --- /dev/null +++ b/.version @@ -0,0 +1 @@ +0.0.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..2a6985a7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,73 @@ +# Contributing + +I am happy that you want to contribute to Pocket ID and help to make it better! All contributions are welcome, including issues, suggestions, pull requests and more. + +## Getting started + +You've found a bug, have suggestion or something else, just create an issue on GitHub and we can get in touch. + +## Submit a Pull Request + +Before you submit the pull request for review please ensure that + +- The pull request naming follows the [Conventional Commits specification](https://www.conventionalcommits.org): + + `[optional scope]: ` + + example: + + ``` + feat(share): add password protection + ``` + + Where `TYPE` can be: + + - **feat** - is a new feature + - **doc** - documentation only changes + - **fix** - a bug fix + - **refactor** - code change that neither fixes a bug nor adds a feature + +- Your pull request has a detailed description +- You run `npm run format` to format the code + +## Setup project + +Pocket ID consists of a frontend, backend and a reverse proxy. + +### Backend + +The backend is built with [Gin](https://gin-gonic.com) and written in Go. + +#### Setup + +1. Open the `backend` folder +2. Copy the `.env.example` file to `.env` and change the `APP_ENV` to `development` +3. Start the backend with `go run cmd/main.go` + +### Frontend + +The frontend is built with [SvelteKit](https://kit.svelte.dev) and written in TypeScript. + +#### Setup + +1. Open the `frontend` folder +2. Copy the `.env.example` file to `.env` +3. Install the dependencies with `npm install` +4. Start the frontend with `npm run dev` + +You're all set! + +### Reverse Proxy +We use [Caddy](https://caddyserver.com) as a reverse proxy. You can use any other reverse proxy if you want but you have to configure it yourself. + +#### Setup +Run `caddy run --config Caddyfile` in the root folder. + +### Testing + +We are using [Playwright](https://playwright.dev) for end-to-end testing. + +The tests can be run like this: +1. Start the backend normally +2. Start the frontend in production mode with `npm run build && node build/index.js` +3. Run the tests with `npm run test` diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 00000000..4ae284ab --- /dev/null +++ b/Caddyfile @@ -0,0 +1,10 @@ +:80 { + reverse_proxy /api/* http://localhost:8080 + reverse_proxy /.well-known/* http://localhost:8080 + reverse_proxy /* http://localhost:3000 + + log { + output file /var/log/caddy/access.log + level WARN + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c47149ad --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +# Stage 1: Build Frontend +FROM node:20-alpine AS frontend-builder +WORKDIR /app/frontend +COPY ./frontend/package*.json ./ +RUN npm ci +COPY ./frontend ./ +RUN npm run build +RUN npm prune --production + +# Stage 2: Build Backend +FROM golang:1.22-alpine AS backend-builder +WORKDIR /app/backend +COPY ./backend/go.mod ./backend/go.sum ./ +RUN go mod download + +RUN apk add --no-cache gcc musl-dev + +COPY ./backend ./ +WORKDIR /app/backend/cmd +RUN CGO_ENABLED=1 GOOS=linux go build -o /app/backend/pocket-id-backend . + +# Stage 3: Production Image +FROM node:20-alpine +RUN apk add --no-cache caddy +COPY ./Caddyfile /etc/caddy/Caddyfile + +WORKDIR /app +COPY --from=frontend-builder /app/frontend/build ./frontend/build +COPY --from=frontend-builder /app/frontend/node_modules ./frontend/node_modules +COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json + +COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend +COPY --from=backend-builder /app/backend/migrations ./backend/migrations +COPY --from=backend-builder /app/backend/images ./backend/images + +COPY ./scripts ./scripts + +EXPOSE 3000 +ENV APP_ENV=production + +# Use a shell form to run both the frontend and backend +CMD ["sh", "./scripts/docker-entrypoint.sh"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7f166a1f --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2024, Elias Schneider +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..abc34e2a --- /dev/null +++ b/README.md @@ -0,0 +1,139 @@ +#

Pocket ID
+ +Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services. + + + +The goal of Pocket ID is to be a simple and easy-to-use. There are other self-hosted OIDC providers like [Keycloak](https://www.keycloak.org/) or [ORY Hydra](https://www.ory.sh/hydra/) but they are often too complex for simple use cases. Additionally, Pocket ID only support passkey authentication which is a passwordless authentication method. + +## Setup + +> [!WARNING] +> Pocket ID is in its early stages and may contain bugs. + +### Installation with Docker (recommended) + +1. Download the `docker-compose.yml` and `.env` file: + + ```bash + curl -O https://raw.githubusercontent.com/stonith404/pocket-id/main/docker-compose.yml + + curl -o .env https://raw.githubusercontent.com/stonith404/pocket-id/main/.env.example + ``` + +2. Edit the `.env` file so that it fits your needs. See the [environment variables](#environment-variables) section for more information. +3. Run `docker compose up -d` + +You can now sign in with the admin account on `http://localhost/login/setup`. + +### Stand-alone Installation + +Required tools: + +- [Node.js](https://nodejs.org/en/download/) >= 20 +- [Go](https://golang.org/doc/install) >= 1.22 +- [Git](https://git-scm.com/downloads) +- [PM2](https://pm2.keymetrics.io/) +- [Caddy](https://caddyserver.com/docs/install) (optional) + +1. Copy the `.env.example` file in the `frontend` and `backend` folder to `.env` and change it so that it fits your needs. + + ```bash + cp frontend/.env.example frontend/.env + cp backend/.env.example backend/.env + ``` +2. Run the following commands: + + ```bash + git clone https://github.com/stonith404/pocket-id + cd pocket-id + + # Checkout the latest version + git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`) + + # Start the backend + cd backend/cmd + go build -o ../pocket-id-backend + cd .. + pm2 start pocket-id-backend --name pocket-id-backend + + # Start the frontend + cd ../frontend + npm install + npm run build + pm2 start --name pocket-id-frontend --node-args="--env-file .env" build/index.js + + # Optional: Start Caddy (You can use any other reverse proxy) + cd .. + pm2 start --name pocket-id-caddy caddy -- run --config Caddyfile + ``` + +You can now sign in with the admin account on `http://localhost/login/setup`. + +### Add Pocket ID as an OIDC provider + +You can add a new OIDC client on `https:///settings/admin/oidc-clients` + +After you have added the client, you can obtain the client ID and client secret. + +You may need the following information: + +- **Authorization URL**: `https:///authorize` +- **Token URL**: `https:///api/oidc/token` +- **Certificate URL**: `https:///.well-known/jwks.json` +- **OIDC Discovery URL**: `https:///.well-known/openid-configuration` +- **PKCE**: `false` as this is not supported yet. + +### Update + +#### Docker + +```bash +docker compose pull +docker compose up -d +``` + +#### Stand-alone + +1. Stop the running services: + ```bash + pm2 delete pocket-id-backend pocket-id-frontend pocket-id-caddy + ``` +2. Run the following commands: + + ```bash + cd pocket-id + + # Checkout the latest version + git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`) + + # Start the backend + cd backend/cmd + go build -o ../pocket-id-backend + cd .. + pm2 start pocket-id-backend --name pocket-id-backend + + # Start the frontend + cd ../frontend + npm install + npm run build + pm2 start build/index.js --name pocket-id-frontend + + # Optional: Start Caddy (You can use any other reverse proxy) + cd .. + pm2 start caddy --name pocket-id-caddy -- run --config Caddyfile + ``` + +### Environment variables + +| Variable | Default Value | Recommended to change | Description | +| ---------------- | ------------------- | --------------------- | --------------------------------------------- | +| `PUBLIC_APP_URL` | `http://localhost` | yes | The URL where you will access the app. | +| `DB_PATH` | `data/pocket-id.db` | no | The path to the SQLite database. | +| `UPLOAD_PATH` | `data/uploads` | no | The path where the uploaded files are stored. | +| `PORT` | `3000` | no | The port on which the frontend should listen. | +| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. | + +## Contribute + +You're very welcome to contribute to Pocket ID! Please follow the [contribution guide](/CONTRIBUTING.md) to get started. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..92ef9728 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Supported Versions + +As Pocket ID is in its early stages, older versions don't get security updates. Please consider to update Pocket ID regularly. Updates can be automated with e.g [Watchtower](https://github.com/containrrr/watchtower). + +## Reporting a Vulnerability + +Thank you for taking the time to report a vulnerability. Please DO NOT create an issue on GitHub because the vulnerability could get exploited. Instead please write an email to [elias@eliasschneider.com](mailto:elias@eliasschneider.com). diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..9fd8c8fa --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,6 @@ +APP_ENV=production +PUBLIC_APP_URL=http://localhost +DB_PATH=data/pocket-id.db +UPLOAD_PATH=data/uploads +PORT=8080 +HOST=localhost \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 00000000..71f540d8 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,16 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +./data \ No newline at end of file diff --git a/backend/cmd/main.go b/backend/cmd/main.go new file mode 100644 index 00000000..6d192187 --- /dev/null +++ b/backend/cmd/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "golang-rest-api-template/internal/bootstrap" +) + +func main() { + bootstrap.Bootstrap() +} diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 00000000..2c8dce86 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,63 @@ +module golang-rest-api-template + +go 1.22 + +require ( + github.com/caarlos0/env/v11 v11.2.0 + github.com/fxamacker/cbor/v2 v2.7.0 + github.com/gin-contrib/cors v1.7.2 + github.com/gin-gonic/gin v1.10.0 + github.com/go-co-op/gocron/v2 v2.11.0 + github.com/go-webauthn/webauthn v0.11.0 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-migrate/migrate/v4 v4.17.1 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + golang.org/x/crypto v0.25.0 + golang.org/x/time v0.6.0 + gorm.io/driver/sqlite v1.5.6 + gorm.io/gorm v1.25.11 +) + +require ( + github.com/bytedance/sonic v1.12.1 // indirect + github.com/bytedance/sonic/loader v0.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/go-webauthn/x v0.1.12 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/go-tpm v0.9.1 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/arch v0.9.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 00000000..a62d61ee --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,151 @@ +github.com/bytedance/sonic v1.12.1 h1:jWl5Qz1fy7X1ioY74WqO0KjAMtAGQs4sYnjiEBiyX24= +github.com/bytedance/sonic v1.12.1/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= +github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/caarlos0/env/v11 v11.2.0 h1:kvB1ZmwdWgI3JsuuVUE7z4cY/6Ujr03D0w2WkOOH4Xs= +github.com/caarlos0/env/v11 v11.2.0/go.mod h1:LwgkYk1kDvfGpHthrWWLof3Ny7PezzFwS4QrsJdHTMo= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= +github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-co-op/gocron/v2 v2.11.0 h1:IOowNA6SzwdRFnD4/Ol3Kj6G2xKfsoiiGq2Jhhm9bvE= +github.com/go-co-op/gocron/v2 v2.11.0/go.mod h1:xY7bJxGazKam1cz04EebrlP4S9q4iWdiAylMGP3jY9w= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-webauthn/webauthn v0.11.0 h1:2U0jWuGeoiI+XSZkHPFRtwaYtqmMUsqABtlfSq1rODo= +github.com/go-webauthn/webauthn v0.11.0/go.mod h1:57ZrqsZzD/eboQDVtBkvTdfqFYAh/7IwzdPT+sPWqB0= +github.com/go-webauthn/x v0.1.12 h1:RjQ5cvApzyU/xLCiP+rub0PE4HBZsLggbxGR5ZpUf/A= +github.com/go-webauthn/x v0.1.12/go.mod h1:XlRcGkNH8PT45TfeJYc6gqpOtiOendHhVmnOxh+5yHs= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= +github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-tpm v0.9.1 h1:0pGc4X//bAlmZzMKf8iz6IsDo1nYTbYJ6FZN/rg4zdM= +github.com/google/go-tpm v0.9.1/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k= +golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= +gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/backend/images/background.jpg b/backend/images/background.jpg new file mode 100644 index 00000000..6e59f9aa Binary files /dev/null and b/backend/images/background.jpg differ diff --git a/backend/images/favicon.ico b/backend/images/favicon.ico new file mode 100644 index 00000000..1fea4cef Binary files /dev/null and b/backend/images/favicon.ico differ diff --git a/backend/images/logo.svg b/backend/images/logo.svg new file mode 100644 index 00000000..6459f7b0 --- /dev/null +++ b/backend/images/logo.svg @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/backend/internal/bootstrap/bootstrap.go b/backend/internal/bootstrap/bootstrap.go new file mode 100644 index 00000000..32fbd55a --- /dev/null +++ b/backend/internal/bootstrap/bootstrap.go @@ -0,0 +1,78 @@ +package bootstrap + +import ( + "github.com/gin-gonic/gin" + _ "github.com/golang-migrate/migrate/v4/source/file" + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/common/middleware" + "golang-rest-api-template/internal/handler" + "golang-rest-api-template/internal/job" + "golang-rest-api-template/internal/utils" + "golang.org/x/time/rate" + "log" + "os" + "time" +) + +func Bootstrap() { + common.InitDatabase() + common.InitDbConfig() + initApplicationImages() + job.RegisterJobs() + initRouter() +} + +func initRouter() { + switch common.EnvConfig.AppEnv { + case "production": + gin.SetMode(gin.ReleaseMode) + case "development": + gin.SetMode(gin.DebugMode) + case "test": + gin.SetMode(gin.TestMode) + } + + r := gin.Default() + + r.Use(gin.Logger()) + + r.Use(middleware.Cors()) + r.Use(middleware.RateLimiter(rate.Every(time.Second), 60)) + + apiGroup := r.Group("/api") + handler.RegisterRoutes(apiGroup) + handler.RegisterOIDCRoutes(apiGroup) + handler.RegisterUserRoutes(apiGroup) + handler.RegisterConfigurationRoutes(apiGroup) + if common.EnvConfig.AppEnv != "production" { + handler.RegisterTestRoutes(apiGroup) + } + + baseGroup := r.Group("/") + handler.RegisterWellKnownRoutes(baseGroup) + + if err := r.Run(common.EnvConfig.Host + ":" + common.EnvConfig.Port); err != nil { + log.Fatal(err) + } + +} + +func initApplicationImages() { + dirPath := common.EnvConfig.UploadPath + "/application-images" + + files, err := os.ReadDir(dirPath) + if err != nil && !os.IsNotExist(err) { + log.Fatalf("Error reading directory: %v", err) + } + + // Skip if files already exist + if len(files) > 1 { + return + } + + // Copy files from source to destination + err = utils.CopyDirectory("./images", dirPath) + if err != nil { + log.Fatalf("Error copying directory: %v", err) + } +} diff --git a/backend/internal/common/config.go b/backend/internal/common/config.go new file mode 100644 index 00000000..d1262ac2 --- /dev/null +++ b/backend/internal/common/config.go @@ -0,0 +1,133 @@ +package common + +import ( + "github.com/caarlos0/env/v11" + _ "github.com/joho/godotenv/autoload" + "golang-rest-api-template/internal/model" + "log" + "reflect" +) + +type EnvConfigSchema struct { + AppEnv string `env:"APP_ENV"` + AppURL string `env:"PUBLIC_APP_URL"` + DBPath string `env:"DB_PATH"` + UploadPath string `env:"UPLOAD_PATH"` + Port string `env:"BACKEND_PORT"` + Host string `env:"HOST"` +} + +var EnvConfig = &EnvConfigSchema{ + AppEnv: "production", + DBPath: "data/pocket-id.db", + UploadPath: "data/uploads", + AppURL: "http://localhost", + Port: "8080", + Host: "localhost", +} + +var DbConfig = NewDefaultDbConfig() + +func NewDefaultDbConfig() model.ApplicationConfiguration { + return model.ApplicationConfiguration{ + AppName: model.ApplicationConfigurationVariable{ + Key: "appName", + Type: "string", + IsPublic: true, + Value: "Pocket ID", + }, + BackgroundImageType: model.ApplicationConfigurationVariable{ + Key: "backgroundImageType", + Type: "string", + IsInternal: true, + Value: "jpg", + }, + LogoImageType: model.ApplicationConfigurationVariable{ + Key: "logoImageType", + Type: "string", + IsInternal: true, + Value: "svg", + }, + } +} + +// LoadDbConfigFromDb refreshes the database configuration by loading the current values +// from the database and updating the DbConfig struct. +func LoadDbConfigFromDb() error { + dbConfigReflectValue := reflect.ValueOf(&DbConfig).Elem() + + for i := 0; i < dbConfigReflectValue.NumField(); i++ { + dbConfigField := dbConfigReflectValue.Field(i) + currentConfigVar := dbConfigField.Interface().(model.ApplicationConfigurationVariable) + var storedConfigVar model.ApplicationConfigurationVariable + if err := DB.First(&storedConfigVar, "key = ?", currentConfigVar.Key).Error; err != nil { + return err + } + + dbConfigField.Set(reflect.ValueOf(storedConfigVar)) + } + + return nil +} + +// InitDbConfig creates the default configuration values in the database if they do not exist, +// updates existing configurations if they differ from the default, and deletes any configurations +// that are not in the default configuration. +func InitDbConfig() { + // Reflect to get the underlying value of DbConfig and its default configuration + dbConfigReflectValue := reflect.ValueOf(&DbConfig).Elem() + defaultDbConfig := NewDefaultDbConfig() + defaultConfigReflectValue := reflect.ValueOf(&defaultDbConfig).Elem() + defaultKeys := make(map[string]struct{}) + + // Iterate over the fields of DbConfig + for i := 0; i < dbConfigReflectValue.NumField(); i++ { + dbConfigField := dbConfigReflectValue.Field(i) + currentConfigVar := dbConfigField.Interface().(model.ApplicationConfigurationVariable) + defaultConfigVar := defaultConfigReflectValue.Field(i).Interface().(model.ApplicationConfigurationVariable) + defaultKeys[currentConfigVar.Key] = struct{}{} + + var storedConfigVar model.ApplicationConfigurationVariable + if err := DB.First(&storedConfigVar, "key = ?", currentConfigVar.Key).Error; err != nil { + // If the configuration does not exist, create it + if err := DB.Create(&defaultConfigVar).Error; err != nil { + log.Fatalf("Failed to create default configuration: %v", err) + } + dbConfigField.Set(reflect.ValueOf(defaultConfigVar)) + continue + } + + // Update existing configuration if it differs from the default + if storedConfigVar.Type != defaultConfigVar.Type || storedConfigVar.IsPublic != defaultConfigVar.IsPublic || storedConfigVar.IsInternal != defaultConfigVar.IsInternal { + storedConfigVar.Type = defaultConfigVar.Type + storedConfigVar.IsPublic = defaultConfigVar.IsPublic + storedConfigVar.IsInternal = defaultConfigVar.IsInternal + if err := DB.Save(&storedConfigVar).Error; err != nil { + log.Fatalf("Failed to update configuration: %v", err) + } + } + + // Set the value in DbConfig + dbConfigField.Set(reflect.ValueOf(storedConfigVar)) + } + + // Delete any configurations not in the default keys + var allConfigVars []model.ApplicationConfigurationVariable + if err := DB.Find(&allConfigVars).Error; err != nil { + log.Fatalf("Failed to retrieve existing configurations: %v", err) + } + + for _, config := range allConfigVars { + if _, exists := defaultKeys[config.Key]; !exists { + if err := DB.Delete(&config).Error; err != nil { + log.Fatalf("Failed to delete outdated configuration: %v", err) + } + } + } +} + +func init() { + if err := env.ParseWithOptions(EnvConfig, env.Options{}); err != nil { + log.Fatal(err) + } +} diff --git a/backend/internal/common/db.go b/backend/internal/common/db.go new file mode 100644 index 00000000..3c3cf1c2 --- /dev/null +++ b/backend/internal/common/db.go @@ -0,0 +1,84 @@ +package common + +import ( + "errors" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/sqlite3" + "gorm.io/gorm/logger" + "log" + "os" + "time" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func InitDatabase() { + connectDatabase() + sqlDb, err := DB.DB() + if err != nil { + log.Fatal("failed to get sql db", err) + } + driver, err := sqlite3.WithInstance(sqlDb, &sqlite3.Config{}) + m, err := migrate.NewWithDatabaseInstance( + "file://migrations", + "postgres", driver) + if err != nil { + log.Fatal("failed to create migration instance", err) + } + + err = m.Up() + if err != nil && !errors.Is(err, migrate.ErrNoChange) { + log.Fatal("failed to run migrations", err) + } +} + +func connectDatabase() { + var database *gorm.DB + var err error + + dbPath := EnvConfig.DBPath + if EnvConfig.AppEnv == "test" { + dbPath = "file::memory:?cache=shared" + } + + for i := 1; i <= 3; i++ { + database, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + TranslateError: true, + Logger: getLogger(), + }) + if err == nil { + break + } else { + log.Printf("Attempt %d: Failed to initialize database. Retrying...", i) + time.Sleep(3 * time.Second) + } + } + + DB = database +} + +func getLogger() logger.Interface { + isProduction := EnvConfig.AppEnv == "production" + + var logLevel logger.LogLevel + if isProduction { + logLevel = logger.Error + } else { + logLevel = logger.Info + } + + // Create the GORM logger + return logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: logLevel, + IgnoreRecordNotFoundError: isProduction, + ParameterizedQueries: isProduction, + Colorful: !isProduction, + }, + ) +} diff --git a/backend/internal/common/jwt.go b/backend/internal/common/jwt.go new file mode 100644 index 00000000..41e44905 --- /dev/null +++ b/backend/internal/common/jwt.go @@ -0,0 +1,207 @@ +package common + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "github.com/golang-jwt/jwt/v5" + "golang-rest-api-template/internal/model" + "golang-rest-api-template/internal/utils" + "log" + "math/big" + "os" + "path/filepath" + "slices" + "strings" + "time" +) + +var ( + PrivateKey *rsa.PrivateKey + PublicKey *rsa.PublicKey +) + +const ( + privateKeyPath = "data/keys/jwt_private_key.pem" + publicKeyPath = "data/keys/jwt_public_key.pem" +) + +type accessTokenJWTClaims struct { + jwt.RegisteredClaims + IsAdmin bool `json:"isAdmin,omitempty"` +} + +// GenerateIDToken generates an ID token for the given user, clientID, scope and nonce. +func GenerateIDToken(user model.User, clientID string, scope string, nonce string) (tokenString string, err error) { + profileClaims := map[string]interface{}{ + "given_name": user.FirstName, + "family_name": user.LastName, + "email": user.Email, + "preferred_username": user.Username, + } + + claims := jwt.MapClaims{ + "sub": user.ID, + "aud": clientID, + "exp": jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), + "iat": jwt.NewNumericDate(time.Now()), + } + + if nonce != "" { + claims["nonce"] = nonce + } + if strings.Contains(scope, "profile") { + for k, v := range profileClaims { + claims[k] = v + } + } + if strings.Contains(scope, "email") { + claims["email"] = user.Email + } + + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) + signedToken, err := token.SignedString(PrivateKey) + if err != nil { + return "", err + } + + return signedToken, nil +} + +// GenerateAccessToken generates an access token for the given user. +func GenerateAccessToken(user model.User) (tokenString string, err error) { + claim := accessTokenJWTClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Subject: user.ID, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + Audience: jwt.ClaimStrings{utils.GetHostFromURL(EnvConfig.AppURL)}, + }, + IsAdmin: user.IsAdmin, + } + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claim) + tokenString, err = token.SignedString(PrivateKey) + return tokenString, err +} + +// VerifyAccessToken verifies the given access token and returns the claims if the token is valid. +func VerifyAccessToken(tokenString string) (*accessTokenJWTClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &accessTokenJWTClaims{}, func(token *jwt.Token) (interface{}, error) { + return PublicKey, nil + }) + if err != nil || !token.Valid { + return nil, errors.New("couldn't handle this token") + } + + claims, isValid := token.Claims.(*accessTokenJWTClaims) + if !isValid { + return nil, errors.New("can't parse claims") + } + + if !slices.Contains(claims.Audience, utils.GetHostFromURL(EnvConfig.AppURL)) { + return nil, errors.New("audience doesn't match") + } + return claims, nil +} + +type JWK struct { + Kty string `json:"kty"` + Use string `json:"use"` + Kid string `json:"kid"` + Alg string `json:"alg"` + N string `json:"n"` + E string `json:"e"` +} + +// GetJWK returns the JSON Web Key (JWK) for the public key. +func GetJWK() (JWK, error) { + if PublicKey == nil { + return JWK{}, errors.New("public key is not initialized") + } + + // Create JWK from RSA public key + jwk := JWK{ + Kty: "RSA", + Use: "sig", + Kid: "1", // Key ID can be set to any identifier. Here it's statically set to "1" + Alg: "RS256", + N: base64.RawURLEncoding.EncodeToString(PublicKey.N.Bytes()), + E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(PublicKey.E)).Bytes()), + } + + return jwk, nil +} + +// generateKeys generates a new RSA key pair and saves the private and public keys to the data folder. +func generateKeys() { + if err := os.MkdirAll(filepath.Dir(privateKeyPath), 0700); err != nil { + log.Fatal("Failed to create directories for keys", err) + } + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log.Fatal("Failed to generate private key", err) + } + + privateKeyFile, err := os.Create(privateKeyPath) + if err != nil { + log.Fatal("Failed to create private key file", err) + } + defer privateKeyFile.Close() + + privateKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + }, + ) + _, err = privateKeyFile.Write(privateKeyPEM) + if err != nil { + log.Fatal("Failed to write private key file", err) + } + + publicKey := &privateKey.PublicKey + publicKeyFile, err := os.Create(publicKeyPath) + if err != nil { + log.Fatal("Failed to create public key file", err) + } + defer publicKeyFile.Close() + + publicKeyPEM := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(publicKey), + }, + ) + _, err = publicKeyFile.Write(publicKeyPEM) + if err != nil { + log.Fatal("Failed to write public key file", err) + } +} + +func init() { + if _, err := os.Stat(privateKeyPath); os.IsNotExist(err) { + generateKeys() + } + + privateKeyBytes, err := os.ReadFile(privateKeyPath) + if err != nil { + log.Fatal("Can't read jwt private key", err) + } + PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes) + if err != nil { + log.Fatal("Can't parse jwt private key", err) + } + + publicKeyBytes, err := os.ReadFile(publicKeyPath) + if err != nil { + log.Fatal("Can't read jwt public key", err) + } + PublicKey, err = jwt.ParseRSAPublicKeyFromPEM(publicKeyBytes) + if err != nil { + log.Fatal("Can't parse jwt public key", err) + } +} diff --git a/backend/internal/common/middleware/cors.go b/backend/internal/common/middleware/cors.go new file mode 100644 index 00000000..30d478d3 --- /dev/null +++ b/backend/internal/common/middleware/cors.go @@ -0,0 +1,18 @@ +package middleware + +import ( + "golang-rest-api-template/internal/common" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func Cors() gin.HandlerFunc { + return cors.New(cors.Config{ + AllowOrigins: []string{common.EnvConfig.AppURL}, + AllowMethods: []string{"*"}, + AllowHeaders: []string{"*"}, + MaxAge: 12 * time.Hour, + }) +} diff --git a/backend/internal/common/middleware/file_size_limit.go b/backend/internal/common/middleware/file_size_limit.go new file mode 100644 index 00000000..5fd2dcf9 --- /dev/null +++ b/backend/internal/common/middleware/file_size_limit.go @@ -0,0 +1,40 @@ +package middleware + +import ( + "fmt" + "github.com/gin-gonic/gin" + "golang-rest-api-template/internal/utils" + "net/http" +) + +func LimitFileSize(maxSize int64) gin.HandlerFunc { + return func(c *gin.Context) { + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize) + if err := c.Request.ParseMultipartForm(maxSize); err != nil { + utils.HandlerError(c, http.StatusRequestEntityTooLarge, fmt.Sprintf("The file can't be larger than %s bytes", formatFileSize(maxSize))) + c.Abort() + return + } + c.Next() + } +} + +// formatFileSize formats a file size in bytes to a human-readable string +func formatFileSize(size int64) string { + const ( + KB = 1 << (10 * 1) + MB = 1 << (10 * 2) + GB = 1 << (10 * 3) + ) + + switch { + case size >= GB: + return fmt.Sprintf("%.2f GB", float64(size)/GB) + case size >= MB: + return fmt.Sprintf("%.2f MB", float64(size)/MB) + case size >= KB: + return fmt.Sprintf("%.2f KB", float64(size)/KB) + default: + return fmt.Sprintf("%d bytes", size) + } +} diff --git a/backend/internal/common/middleware/jwt_auth.go b/backend/internal/common/middleware/jwt_auth.go new file mode 100644 index 00000000..f88f4383 --- /dev/null +++ b/backend/internal/common/middleware/jwt_auth.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/utils" + "net/http" + "strings" +) + +func JWTAuth(adminOnly bool) gin.HandlerFunc { + return func(c *gin.Context) { + + // Extract the token from the cookie or the Authorization header + token, err := c.Cookie("access_token") + if err != nil { + authorizationHeaderSplitted := strings.Split(c.GetHeader("Authorization"), " ") + if len(authorizationHeaderSplitted) == 2 { + token = authorizationHeaderSplitted[1] + } else { + utils.HandlerError(c, http.StatusUnauthorized, "You're not signed in") + c.Abort() + return + } + + } + + // Verify the token + claims, err := common.VerifyAccessToken(token) + if err != nil { + utils.HandlerError(c, http.StatusUnauthorized, "You're not signed in") + c.Abort() + return + } + + // Check if the user is an admin + if adminOnly && !claims.IsAdmin { + utils.HandlerError(c, http.StatusForbidden, "You don't have permission to access this resource") + c.Abort() + return + } + + c.Set("userID", claims.Subject) + c.Set("userIsAdmin", claims.IsAdmin) + c.Next() + } +} diff --git a/backend/internal/common/middleware/rate_limit.go b/backend/internal/common/middleware/rate_limit.go new file mode 100644 index 00000000..b133a3cd --- /dev/null +++ b/backend/internal/common/middleware/rate_limit.go @@ -0,0 +1,76 @@ +package middleware + +import ( + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/utils" + "net/http" + "sync" + "time" + + "github.com/gin-gonic/gin" + "golang.org/x/time/rate" +) + +// RateLimiter is a Gin middleware for rate limiting based on client IP +func RateLimiter(limit rate.Limit, burst int) gin.HandlerFunc { + // Start the cleanup routine + go cleanupClients() + + return func(c *gin.Context) { + ip := c.ClientIP() + + // Skip rate limiting for localhost and test environment + // If the client ip is localhost the request comes from the frontend + if ip == "127.0.0.1" || ip == "::1" || common.EnvConfig.AppEnv == "test" { + c.Next() + return + } + + limiter := getLimiter(ip, limit, burst) + if !limiter.Allow() { + utils.HandlerError(c, http.StatusTooManyRequests, "Too many requests. Please wait a while before trying again.") + c.Abort() + return + } + + c.Next() + } +} + +type client struct { + limiter *rate.Limiter + lastSeen time.Time +} + +// Map to store the rate limiters per IP +var clients = make(map[string]*client) +var mu sync.Mutex + +// Cleanup routine to remove stale clients that haven't been seen for a while +func cleanupClients() { + for { + time.Sleep(time.Minute) + mu.Lock() + for ip, client := range clients { + if time.Since(client.lastSeen) > 3*time.Minute { + delete(clients, ip) + } + } + mu.Unlock() + } +} + +// getLimiter retrieves the rate limiter for a given IP address, creating one if it doesn't exist +func getLimiter(ip string, limit rate.Limit, burst int) *rate.Limiter { + mu.Lock() + defer mu.Unlock() + + if client, exists := clients[ip]; exists { + client.lastSeen = time.Now() + return client.limiter + } + + limiter := rate.NewLimiter(limit, burst) + clients[ip] = &client{limiter: limiter, lastSeen: time.Now()} + return limiter +} diff --git a/backend/internal/common/webauthn.go b/backend/internal/common/webauthn.go new file mode 100644 index 00000000..2c966a40 --- /dev/null +++ b/backend/internal/common/webauthn.go @@ -0,0 +1,37 @@ +package common + +import ( + "github.com/go-webauthn/webauthn/webauthn" + "golang-rest-api-template/internal/utils" + "log" + "time" +) + +var ( + WebAuthn *webauthn.WebAuthn + err error +) + +func init() { + config := &webauthn.Config{ + RPDisplayName: DbConfig.AppName.Value, + RPID: utils.GetHostFromURL(EnvConfig.AppURL), + RPOrigins: []string{EnvConfig.AppURL}, + Timeouts: webauthn.TimeoutsConfig{ + Login: webauthn.TimeoutConfig{ + Enforce: true, + Timeout: time.Second * 60, + TimeoutUVD: time.Second * 60, + }, + Registration: webauthn.TimeoutConfig{ + Enforce: true, + Timeout: time.Second * 60, + TimeoutUVD: time.Second * 60, + }, + }, + } + + if WebAuthn, err = webauthn.New(config); err != nil { + log.Fatal(err) + } +} diff --git a/backend/internal/handler/application_configuration.go b/backend/internal/handler/application_configuration.go new file mode 100644 index 00000000..c952059f --- /dev/null +++ b/backend/internal/handler/application_configuration.go @@ -0,0 +1,190 @@ +package handler + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/common/middleware" + "golang-rest-api-template/internal/model" + "golang-rest-api-template/internal/utils" + "gorm.io/gorm" + "net/http" + "os" + "reflect" +) + +func RegisterConfigurationRoutes(group *gin.RouterGroup) { + group.GET("/application-configuration", listApplicationConfigurationHandler) + group.PUT("/application-configuration", updateApplicationConfigurationHandler) + + group.GET("/application-configuration/logo", getLogoHandler) + group.GET("/application-configuration/background-image", getBackgroundImageHandler) + group.GET("/application-configuration/favicon", getFaviconHandler) + group.PUT("/application-configuration/logo", middleware.JWTAuth(true), updateLogoHandler) + group.PUT("/application-configuration/favicon", middleware.JWTAuth(true), updateFaviconHandler) + group.PUT("/application-configuration/background-image", middleware.JWTAuth(true), updateBackgroundImageHandler) +} + +func listApplicationConfigurationHandler(c *gin.Context) { + // Return also the private configuration variables if the user is admin and showAll is true + showAll := c.GetBool("userIsAdmin") && c.DefaultQuery("showAll", "false") == "true" + + var configuration []model.ApplicationConfigurationVariable + var err error + + if showAll { + err = common.DB.Find(&configuration).Error + } else { + err = common.DB.Find(&configuration, "is_public = true").Error + } + + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(200, configuration) +} + +func updateApplicationConfigurationHandler(c *gin.Context) { + var input model.ApplicationConfigurationUpdateDto + if err := c.ShouldBindJSON(&input); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + savedConfigVariables := make([]model.ApplicationConfigurationVariable, 10) + + tx := common.DB.Begin() + rt := reflect.ValueOf(input).Type() + rv := reflect.ValueOf(input) + + // Loop over the input struct fields and update the related configuration variables + for i := 0; i < rt.NumField(); i++ { + field := rt.Field(i) + key := field.Tag.Get("json") + value := rv.FieldByName(field.Name).String() + + // Get the existing configuration variable from the db + var applicationConfigurationVariable model.ApplicationConfigurationVariable + if err := tx.First(&applicationConfigurationVariable, "key = ? AND is_internal = false", key).Error; err != nil { + tx.Rollback() + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.HandlerError(c, http.StatusNotFound, fmt.Sprintf("Invalid configuration variable '%s'", value)) + } else { + utils.UnknownHandlerError(c, err) + } + return + } + + // Update the value of the existing configuration variable and save it + applicationConfigurationVariable.Value = value + if err := tx.Save(&applicationConfigurationVariable).Error; err != nil { + tx.Rollback() + utils.UnknownHandlerError(c, err) + return + } + + savedConfigVariables[i] = applicationConfigurationVariable + } + + tx.Commit() + + if err := common.LoadDbConfigFromDb(); err != nil { + utils.UnknownHandlerError(c, err) + } + + c.JSON(http.StatusOK, savedConfigVariables) + +} + +func getLogoHandler(c *gin.Context) { + imagType := common.DbConfig.LogoImageType.Value + getImage(c, "logo", imagType) +} + +func getFaviconHandler(c *gin.Context) { + getImage(c, "favicon", "ico") +} + +func getBackgroundImageHandler(c *gin.Context) { + imageType := common.DbConfig.BackgroundImageType.Value + getImage(c, "background", imageType) +} + +func updateLogoHandler(c *gin.Context) { + imageType := common.DbConfig.LogoImageType.Value + updateImage(c, "logo", imageType) +} + +func updateFaviconHandler(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + fileType := utils.GetFileExtension(file.Filename) + if fileType != "ico" { + utils.HandlerError(c, http.StatusBadRequest, "File must be of type .ico") + return + } + updateImage(c, "favicon", "ico") +} + +func updateBackgroundImageHandler(c *gin.Context) { + imagType := common.DbConfig.BackgroundImageType.Value + updateImage(c, "background", imagType) +} + +func getImage(c *gin.Context, name string, imageType string) { + imagePath := fmt.Sprintf("%s/application-images/%s.%s", common.EnvConfig.UploadPath, name, imageType) + mimeType := utils.GetImageMimeType(imageType) + + c.Header("Content-Type", mimeType) + c.File(imagePath) +} + +func updateImage(c *gin.Context, imageName string, oldImageType string) { + file, err := c.FormFile("file") + if err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + fileType := utils.GetFileExtension(file.Filename) + if mimeType := utils.GetImageMimeType(fileType); mimeType == "" { + utils.HandlerError(c, http.StatusBadRequest, "File type not supported") + return + } + + // Delete the old image if it has a different file type + if fileType != oldImageType { + oldImagePath := fmt.Sprintf("%s/application-images/%s.%s", common.EnvConfig.UploadPath, imageName, oldImageType) + if err := os.Remove(oldImagePath); err != nil { + utils.UnknownHandlerError(c, err) + return + } + } + + imagePath := fmt.Sprintf("%s/application-images/%s.%s", common.EnvConfig.UploadPath, imageName, fileType) + err = c.SaveUploadedFile(file, imagePath) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + // Update the file type in the database + key := fmt.Sprintf("%sImageType", imageName) + err = common.DB.Model(&model.ApplicationConfigurationVariable{}).Where("key = ?", key).Update("value", fileType).Error + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + if err := common.LoadDbConfigFromDb(); err != nil { + utils.UnknownHandlerError(c, err) + } + + c.Status(http.StatusNoContent) +} diff --git a/backend/internal/handler/oidc.go b/backend/internal/handler/oidc.go new file mode 100644 index 00000000..f45c6b38 --- /dev/null +++ b/backend/internal/handler/oidc.go @@ -0,0 +1,415 @@ +package handler + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/common/middleware" + "golang-rest-api-template/internal/model" + "golang-rest-api-template/internal/utils" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" + "net/http" + "os" + "time" +) + +func RegisterOIDCRoutes(group *gin.RouterGroup) { + group.POST("/oidc/authorize", middleware.JWTAuth(false), authorizeHandler) + group.POST("/oidc/authorize/new-client", middleware.JWTAuth(false), authorizeNewClientHandler) + group.POST("/oidc/token", createIDTokenHandler) + + group.GET("/oidc/clients", middleware.JWTAuth(true), listClientsHandler) + group.POST("/oidc/clients", middleware.JWTAuth(true), createClientHandler) + group.GET("/oidc/clients/:id", getClientHandler) + group.PUT("/oidc/clients/:id", middleware.JWTAuth(true), updateClientHandler) + group.DELETE("/oidc/clients/:id", middleware.JWTAuth(true), deleteClientHandler) + + group.POST("/oidc/clients/:id/secret", middleware.JWTAuth(true), createClientSecretHandler) + + group.GET("/oidc/clients/:id/logo", getClientLogoHandler) + group.DELETE("/oidc/clients/:id/logo", deleteClientLogoHandler) + group.POST("/oidc/clients/:id/logo", middleware.JWTAuth(true), middleware.LimitFileSize(2<<20), updateClientLogoHandler) +} + +type AuthorizeRequest struct { + ClientID string `json:"clientID" binding:"required"` + Scope string `json:"scope" binding:"required"` + Nonce string `json:"nonce"` +} + +func authorizeHandler(c *gin.Context) { + var parsedBody AuthorizeRequest + if err := c.ShouldBindJSON(&parsedBody); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + var userAuthorizedOIDCClient model.UserAuthorizedOidcClient + common.DB.First(&userAuthorizedOIDCClient, "client_id = ? AND user_id = ?", parsedBody.ClientID, c.GetString("userID")) + + // If the record isn't found or the scope is different return an error + // The client will have to call the authorizeNewClientHandler + if userAuthorizedOIDCClient.Scope != parsedBody.Scope { + utils.HandlerError(c, http.StatusForbidden, "missing authorization") + return + } + + authorizationCode, err := createAuthorizationCode(parsedBody.ClientID, c.GetString("userID"), parsedBody.Scope, parsedBody.Nonce) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{"code": authorizationCode}) +} + +// authorizeNewClientHandler authorizes a new client for the user +// a new client is a new client when the user has not authorized the client before +func authorizeNewClientHandler(c *gin.Context) { + var parsedBody model.AuthorizeNewClientDto + if err := c.ShouldBindJSON(&parsedBody); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + userAuthorizedClient := model.UserAuthorizedOidcClient{ + UserID: c.GetString("userID"), + ClientID: parsedBody.ClientID, + Scope: parsedBody.Scope, + } + err := common.DB.Create(&userAuthorizedClient).Error + + if err != nil && errors.Is(err, gorm.ErrDuplicatedKey) { + err = common.DB.Model(&userAuthorizedClient).Update("scope", parsedBody.Scope).Error + } + + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + authorizationCode, err := createAuthorizationCode(parsedBody.ClientID, c.GetString("userID"), parsedBody.Scope, parsedBody.Nonce) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{"code": authorizationCode}) + +} + +func createIDTokenHandler(c *gin.Context) { + var body model.OidcIdTokenDto + + if err := c.ShouldBind(&body); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + // Currently only authorization_code grant type is supported + if body.GrantType != "authorization_code" { + utils.HandlerError(c, http.StatusBadRequest, "grant type not supported") + return + } + + clientID := body.ClientID + clientSecret := body.ClientSecret + + // Client id and secret can also be passed over the Authorization header + if clientID == "" || clientSecret == "" { + var ok bool + clientID, clientSecret, ok = c.Request.BasicAuth() + if !ok { + utils.HandlerError(c, http.StatusBadRequest, "Client id and secret not provided") + return + } + } + + // Get the client + var client model.OidcClient + err := common.DB.First(&client, "id = ?", clientID, clientSecret).Error + if err != nil { + utils.HandlerError(c, http.StatusBadRequest, "OIDC OIDC client not found") + return + } + + // Check if client secret is correct + err = bcrypt.CompareHashAndPassword([]byte(client.Secret), []byte(clientSecret)) + if err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid client secret") + return + } + + var authorizationCodeMetaData model.OidcAuthorizationCode + err = common.DB.Preload("User").First(&authorizationCodeMetaData, "code = ?", body.Code).Error + if err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid authorization code") + return + } + + // Check if the client id matches the client id in the authorization code and if the code has expired + if authorizationCodeMetaData.ClientID != clientID && authorizationCodeMetaData.ExpiresAt.Before(time.Now()) { + utils.HandlerError(c, http.StatusBadRequest, "invalid authorization code") + return + } + + idToken, e := common.GenerateIDToken(authorizationCodeMetaData.User, clientID, authorizationCodeMetaData.Scope, authorizationCodeMetaData.Nonce) + if e != nil { + utils.UnknownHandlerError(c, err) + return + } + + // Delete the authorization code after it has been used + common.DB.Delete(&authorizationCodeMetaData) + + c.JSON(http.StatusOK, gin.H{"id_token": idToken}) +} + +func getClientHandler(c *gin.Context) { + clientId := c.Param("id") + + var client model.OidcClient + err := common.DB.First(&client, "id = ?", clientId).Error + if err != nil { + utils.HandlerError(c, http.StatusNotFound, "OIDC client not found") + return + } + + c.JSON(http.StatusOK, client) +} + +func listClientsHandler(c *gin.Context) { + var clients []model.OidcClient + searchTerm := c.Query("search") + + query := common.DB.Model(&model.OidcClient{}) + + if searchTerm != "" { + searchPattern := "%" + searchTerm + "%" + query = query.Where("name LIKE ?", searchPattern) + } + + pagination, err := utils.Paginate(c, query, &clients) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "data": clients, + "pagination": pagination, + }) +} + +func createClientHandler(c *gin.Context) { + var input model.OidcClientCreateDto + if err := c.ShouldBindJSON(&input); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + client := model.OidcClient{ + Name: input.Name, + CallbackURL: input.CallbackURL, + CreatedByID: c.GetString("userID"), + } + + if err := common.DB.Create(&client).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusCreated, client) +} + +func deleteClientHandler(c *gin.Context) { + var client model.OidcClient + if err := common.DB.First(&client, "id = ?", c.Param("id")).Error; err != nil { + utils.HandlerError(c, http.StatusNotFound, "OIDC OIDC client not found") + return + } + + if err := common.DB.Delete(&client).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.Status(http.StatusNoContent) +} + +func updateClientHandler(c *gin.Context) { + var input model.OidcClientCreateDto + if err := c.ShouldBindJSON(&input); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + var client model.OidcClient + if err := common.DB.First(&client, "id = ?", c.Param("id")).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.HandlerError(c, http.StatusNotFound, "OIDC client not found") + return + } + utils.UnknownHandlerError(c, err) + return + } + + client.Name = input.Name + client.CallbackURL = input.CallbackURL + + if err := common.DB.Save(&client).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusNoContent, client) +} + +// createClientSecretHandler creates a new secret for the client and revokes the old one +func createClientSecretHandler(c *gin.Context) { + var client model.OidcClient + if err := common.DB.First(&client, "id = ?", c.Param("id")).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.HandlerError(c, http.StatusNotFound, "OIDC client not found") + return + } + utils.UnknownHandlerError(c, err) + return + } + + clientSecret, err := utils.GenerateRandomAlphanumericString(32) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + hashedSecret, err := bcrypt.GenerateFromPassword([]byte(clientSecret), bcrypt.DefaultCost) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + client.Secret = string(hashedSecret) + if err := common.DB.Save(&client).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{"secret": clientSecret}) +} + +func getClientLogoHandler(c *gin.Context) { + var client model.OidcClient + if err := common.DB.First(&client, "id = ?", c.Param("id")).Error; err != nil { + utils.HandlerError(c, http.StatusNotFound, "OIDC client not found") + return + } + + if client.ImageType == nil { + utils.HandlerError(c, http.StatusNotFound, "image not found") + return + } + + imageType := *client.ImageType + + imagePath := fmt.Sprintf("%s/oidc-client-images/%s.%s", common.EnvConfig.UploadPath, client.ID, imageType) + mimeType := utils.GetImageMimeType(imageType) + + c.Header("Content-Type", mimeType) + c.File(imagePath) +} + +func updateClientLogoHandler(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + fileType := utils.GetFileExtension(file.Filename) + if mimeType := utils.GetImageMimeType(fileType); mimeType == "" { + utils.HandlerError(c, http.StatusBadRequest, "file type not supported") + return + } + + imagePath := fmt.Sprintf("%s/oidc-client-images/%s.%s", common.EnvConfig.UploadPath, c.Param("id"), fileType) + err = c.SaveUploadedFile(file, imagePath) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + var client model.OidcClient + if err := common.DB.First(&client, "id = ?", c.Param("id")).Error; err != nil { + utils.HandlerError(c, http.StatusNotFound, "OIDC client not found") + return + } + + // Delete the old image if it has a different file type + if client.ImageType != nil && fileType != *client.ImageType { + oldImagePath := fmt.Sprintf("%s/oidc-client-images/%s.%s", common.EnvConfig.UploadPath, client.ID, *client.ImageType) + if err := os.Remove(oldImagePath); err != nil { + utils.UnknownHandlerError(c, err) + return + } + } + + client.ImageType = &fileType + if err := common.DB.Save(&client).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.Status(http.StatusNoContent) +} + +func deleteClientLogoHandler(c *gin.Context) { + var client model.OidcClient + if err := common.DB.First(&client, "id = ?", c.Param("id")).Error; err != nil { + utils.HandlerError(c, http.StatusNotFound, "OIDC client not found") + return + } + + if client.ImageType == nil { + utils.HandlerError(c, http.StatusNotFound, "image not found") + return + } + + imagePath := fmt.Sprintf("%s/oidc-client-images/%s.%s", common.EnvConfig.UploadPath, client.ID, *client.ImageType) + if err := os.Remove(imagePath); err != nil { + utils.UnknownHandlerError(c, err) + return + } + + client.ImageType = nil + if err := common.DB.Save(&client).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.Status(http.StatusNoContent) +} + +func createAuthorizationCode(clientID string, userID string, scope string, nonce string) (string, error) { + randomString, err := utils.GenerateRandomAlphanumericString(32) + if err != nil { + return "", err + } + + oidcAuthorizationCode := model.OidcAuthorizationCode{ + ExpiresAt: time.Now().Add(15 * time.Minute), + Code: randomString, + ClientID: clientID, + UserID: userID, + Scope: scope, + Nonce: nonce, + } + + if err := common.DB.Create(&oidcAuthorizationCode).Error; err != nil { + return "", err + } + + return randomString, nil +} diff --git a/backend/internal/handler/test.go b/backend/internal/handler/test.go new file mode 100644 index 00000000..71c1797f --- /dev/null +++ b/backend/internal/handler/test.go @@ -0,0 +1,237 @@ +package handler + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/base64" + "log" + "os" + "time" + + "github.com/fxamacker/cbor/v2" + "github.com/gin-gonic/gin" + "github.com/go-webauthn/webauthn/protocol" + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/model" + "golang-rest-api-template/internal/utils" + "gorm.io/gorm" +) + +func RegisterTestRoutes(group *gin.RouterGroup) { + group.POST("/test/reset", resetAndSeedHandler) +} + +func resetAndSeedHandler(c *gin.Context) { + if err := resetDatabase(); err != nil { + utils.UnknownHandlerError(c, err) + return + } + + if err := resetApplicationImages(); err != nil { + utils.UnknownHandlerError(c, err) + return + } + + if err := seedDatabase(); err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(200, gin.H{"message": "Database reset and seeded"}) +} + +// seedDatabase seeds the database with initial data and uses a transaction to ensure atomicity. +func seedDatabase() error { + return common.DB.Transaction(func(tx *gorm.DB) error { + users := []model.User{ + { + Base: model.Base{ + ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e", + }, + Username: "tim", + Email: "tim.cook@test.com", + FirstName: "Tim", + LastName: "Cook", + IsAdmin: true, + }, + { + Base: model.Base{ + ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036", + }, + Username: "craig", + Email: "craig.federighi@test.com", + FirstName: "Craig", + LastName: "Federighi", + IsAdmin: false, + }, + } + for _, user := range users { + if err := tx.Create(&user).Error; err != nil { + return err + } + } + + oidcClients := []model.OidcClient{ + { + Base: model.Base{ + ID: "3654a746-35d4-4321-ac61-0bdcff2b4055", + }, + Name: "Nextcloud", + Secret: "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC", // w2mUeZISmEvIDMEDvpY0PnxQIpj1m3zY + CallbackURL: "http://nextcloud/auth/callback", + ImageType: utils.StringPointer("png"), + CreatedByID: users[0].ID, + }, + { + Base: model.Base{ + ID: "606c7782-f2b1-49e5-8ea9-26eb1b06d018", + }, + Name: "Immich", + Secret: "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe", // PYjrE9u4v9GVqXKi52eur0eb2Ci4kc0x + CallbackURL: "http://immich/auth/callback", + CreatedByID: users[0].ID, + }, + } + for _, client := range oidcClients { + if err := tx.Create(&client).Error; err != nil { + return err + } + } + + authCode := model.OidcAuthorizationCode{ + Code: "auth-code", + Scope: "openid profile", + Nonce: "nonce", + ExpiresAt: time.Now().Add(1 * time.Hour), + UserID: users[0].ID, + ClientID: oidcClients[0].ID, + } + if err := tx.Create(&authCode).Error; err != nil { + return err + } + + accessToken := model.OneTimeAccessToken{ + Token: "one-time-token", + ExpiresAt: time.Now().Add(1 * time.Hour), + UserID: users[0].ID, + } + if err := tx.Create(&accessToken).Error; err != nil { + return err + } + + userAuthorizedClient := model.UserAuthorizedOidcClient{ + Scope: "openid profile email", + UserID: users[0].ID, + ClientID: oidcClients[0].ID, + } + if err := tx.Create(&userAuthorizedClient).Error; err != nil { + return err + } + + webauthnCredentials := []model.WebauthnCredential{ + { + Name: "Passkey 1", + CredentialID: "test-credential-1", + PublicKey: getCborPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwcOo5KV169KR67QEHrcYkeXE3CCxv2BgwnSq4VYTQxyLtdmKxegexa8JdwFKhKXa2BMI9xaN15BoL6wSCRFJhg=="), + AttestationType: "none", + Transport: model.AuthenticatorTransportList{protocol.Internal}, + UserID: users[0].ID, + }, + { + Name: "Passkey 2", + CredentialID: "test-credential-2", + PublicKey: getCborPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESq/wR8QbBu3dKnpaw/v0mDxFFDwnJ/L5XHSg2tAmq5x1BpSMmIr3+DxCbybVvGRmWGh8kKhy7SMnK91M6rFHTA=="), + AttestationType: "none", + Transport: model.AuthenticatorTransportList{protocol.Internal}, + UserID: users[0].ID, + }, + } + for _, credential := range webauthnCredentials { + if err := tx.Create(&credential).Error; err != nil { + return err + } + } + + webauthnSession := model.WebauthnSession{ + Challenge: "challenge", + ExpiresAt: time.Now().Add(1 * time.Hour), + UserVerification: "preferred", + } + if err := tx.Create(&webauthnSession).Error; err != nil { + return err + } + + return nil + }) +} + +// resetDatabase resets the database by deleting all rows from each table. +func resetDatabase() error { + err := common.DB.Transaction(func(tx *gorm.DB) error { + var tables []string + if err := tx.Raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != 'schema_migrations';").Scan(&tables).Error; err != nil { + return err + } + + for _, table := range tables { + if err := tx.Exec("DELETE FROM " + table).Error; err != nil { + return err + } + } + return nil + }) + if err != nil { + return err + } + common.InitDbConfig() + return nil +} + +// resetApplicationImages resets the application images by removing existing images and replacing them with the default ones +func resetApplicationImages() error { + + if err := os.RemoveAll(common.EnvConfig.UploadPath); err != nil { + log.Printf("Error removing directory: %v", err) + return err + } + + if err := utils.CopyDirectory("./images", common.EnvConfig.UploadPath+"/application-images"); err != nil { + log.Printf("Error copying directory: %v", err) + return err + } + + return nil +} + +// getCborPublicKey decodes a Base64 encoded public key and returns the CBOR encoded COSE key +func getCborPublicKey(base64PublicKey string) []byte { + decodedKey, err := base64.StdEncoding.DecodeString(base64PublicKey) + if err != nil { + log.Fatalf("Failed to decode base64 key: %v", err) + } + + pubKey, err := x509.ParsePKIXPublicKey(decodedKey) + if err != nil { + log.Fatalf("Failed to parse public key: %v", err) + } + + ecdsaPubKey, ok := pubKey.(*ecdsa.PublicKey) + if !ok { + log.Fatalf("Not an ECDSA public key") + } + + coseKey := map[int]interface{}{ + 1: 2, // Key type: EC2 + 3: -7, // Algorithm: ECDSA with SHA-256 + -1: 1, // Curve: P-256 + -2: ecdsaPubKey.X.Bytes(), // X coordinate + -3: ecdsaPubKey.Y.Bytes(), // Y coordinate + } + + cborPublicKey, err := cbor.Marshal(coseKey) + if err != nil { + log.Fatalf("Failed to encode CBOR: %v", err) + } + + return cborPublicKey +} diff --git a/backend/internal/handler/user.go b/backend/internal/handler/user.go new file mode 100644 index 00000000..0601a0f3 --- /dev/null +++ b/backend/internal/handler/user.go @@ -0,0 +1,269 @@ +package handler + +import ( + "errors" + "github.com/gin-gonic/gin" + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/common/middleware" + "golang-rest-api-template/internal/model" + "golang-rest-api-template/internal/utils" + "golang.org/x/time/rate" + "gorm.io/gorm" + "log" + "net/http" + "time" +) + +func RegisterUserRoutes(group *gin.RouterGroup) { + group.GET("/users", middleware.JWTAuth(true), listUsersHandler) + group.GET("/users/me", middleware.JWTAuth(false), getCurrentUserHandler) + group.GET("/users/:id", middleware.JWTAuth(true), getUserHandler) + group.POST("/users", middleware.JWTAuth(true), createUserHandler) + group.PUT("/users/:id", middleware.JWTAuth(true), updateUserHandler) + group.PUT("/users/me", middleware.JWTAuth(false), updateCurrentUserHandler) + group.DELETE("/users/:id", middleware.JWTAuth(true), deleteUserHandler) + + group.POST("/users/:id/one-time-access-token", middleware.JWTAuth(true), createOneTimeAccessTokenHandler) + group.POST("/one-time-access-token/:token", middleware.RateLimiter(rate.Every(10*time.Second), 5), exchangeOneTimeAccessTokenHandler) + group.POST("/one-time-access-token/setup", getSetupAccessTokenHandler) +} + +func listUsersHandler(c *gin.Context) { + var users []model.User + searchTerm := c.Query("search") + + query := common.DB.Model(&model.User{}) + + if searchTerm != "" { + searchPattern := "%" + searchTerm + "%" + query = query.Where("email LIKE ? OR first_name LIKE ? OR username LIKE ?", searchPattern, searchPattern, searchPattern) + } + + pagination, err := utils.Paginate(c, query, &users) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "data": users, + "pagination": pagination, + }) +} + +func getUserHandler(c *gin.Context) { + var user model.User + if err := common.DB.Where("id = ?", c.Param("id")).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.HandlerError(c, http.StatusNotFound, "User not found") + return + } + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusOK, user) +} + +func getCurrentUserHandler(c *gin.Context) { + var user model.User + if err := common.DB.Where("id = ?", c.GetString("userID")).First(&user).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + c.JSON(http.StatusOK, user) + +} + +func deleteUserHandler(c *gin.Context) { + var user model.User + if err := common.DB.Where("id = ?", c.Param("id")).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.HandlerError(c, http.StatusNotFound, "User not found") + return + } + utils.UnknownHandlerError(c, err) + return + } + + if err := common.DB.Delete(&user).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.Status(http.StatusNoContent) +} + +func createUserHandler(c *gin.Context) { + var user model.User + if err := c.ShouldBindJSON(&user); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + if err := common.DB.Create(&user).Error; err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + if err := checkDuplicatedFields(user); err != nil { + utils.HandlerError(c, http.StatusBadRequest, err.Error()) + return + } + } else { + utils.UnknownHandlerError(c, err) + return + } + } + + c.JSON(http.StatusCreated, user) +} + +func updateUserHandler(c *gin.Context) { + updateUser(c, c.Param("id")) +} + +func updateCurrentUserHandler(c *gin.Context) { + updateUser(c, c.GetString("userID")) +} + +func createOneTimeAccessTokenHandler(c *gin.Context) { + var input model.OneTimeAccessTokenCreateDto + if err := c.ShouldBindJSON(&input); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + randomString, err := utils.GenerateRandomAlphanumericString(16) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + oneTimeAccessToken := model.OneTimeAccessToken{ + UserID: input.UserID, + ExpiresAt: input.ExpiresAt, + Token: randomString, + } + + if err := common.DB.Create(&oneTimeAccessToken).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusCreated, gin.H{"token": oneTimeAccessToken}) +} + +func exchangeOneTimeAccessTokenHandler(c *gin.Context) { + var oneTimeAccessToken model.OneTimeAccessToken + if err := common.DB.Where("token = ? AND expires_at > ?", c.Param("token"), utils.FormatDateForDb(time.Now())).Preload("User").First(&oneTimeAccessToken).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.HandlerError(c, http.StatusForbidden, "Token is invalid or expired") + return + } + utils.UnknownHandlerError(c, err) + return + } + + token, err := common.GenerateAccessToken(oneTimeAccessToken.User) + if err != nil { + utils.UnknownHandlerError(c, err) + log.Println(err) + return + } + + if err := common.DB.Delete(&oneTimeAccessToken).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true) + + c.JSON(http.StatusOK, oneTimeAccessToken.User) +} + +// getSetupAccessTokenHandler creates the initial admin user and returns an access token for the user +// This handler is only available if there are no users in the database +func getSetupAccessTokenHandler(c *gin.Context) { + var userCount int64 + if err := common.DB.Model(&model.User{}).Count(&userCount).Error; err != nil { + log.Fatal("failed to count users", err) + } + + // If there are more than one user, we don't need to create the admin user + if userCount > 1 { + utils.HandlerError(c, http.StatusForbidden, "Setup already completed") + return + } + + var user = model.User{ + FirstName: "Admin", + LastName: "Admin", + Username: "admin", + Email: "admin@admin.com", + IsAdmin: true, + } + + // Create the initial admin user if it doesn't exist + if err := common.DB.Model(&model.User{}).Preload("Credentials").FirstOrCreate(&user).Error; err != nil { + log.Fatal("failed to create admin user", err) + } + + // If the user already has credentials, the setup is already completed + if len(user.Credentials) > 0 { + utils.HandlerError(c, http.StatusForbidden, "Setup already completed") + return + } + + token, err := common.GenerateAccessToken(user) + if err != nil { + utils.UnknownHandlerError(c, err) + log.Println(err) + return + } + c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true) + c.JSON(http.StatusOK, user) +} + +func updateUser(c *gin.Context, userID string) { + var user model.User + if err := common.DB.Where("id = ?", userID).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.HandlerError(c, http.StatusNotFound, "User not found") + return + } + utils.UnknownHandlerError(c, err) + return + } + + var updatedUser model.User + if err := c.ShouldBindJSON(&updatedUser); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + if err := common.DB.Model(&user).Updates(&updatedUser).Error; err != nil { + if errors.Is(err, gorm.ErrDuplicatedKey) { + if err := checkDuplicatedFields(user); err != nil { + utils.HandlerError(c, http.StatusBadRequest, err.Error()) + return + } + } else { + utils.UnknownHandlerError(c, err) + return + } + } + + c.JSON(http.StatusOK, updatedUser) +} + +func checkDuplicatedFields(user model.User) error { + var existingUser model.User + + if common.DB.Where("id != ? AND email = ?", user.ID, user.Email).First(&existingUser).Error == nil { + return errors.New("email is already taken") + } + + if common.DB.Where("id != ? AND username = ?", user.ID, user.Username).First(&existingUser).Error == nil { + return errors.New("username is already taken") + } + + return nil +} diff --git a/backend/internal/handler/webauthn.go b/backend/internal/handler/webauthn.go new file mode 100644 index 00000000..e94db1fc --- /dev/null +++ b/backend/internal/handler/webauthn.go @@ -0,0 +1,255 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/common/middleware" + "golang-rest-api-template/internal/model" + "golang-rest-api-template/internal/utils" + "golang.org/x/time/rate" + "gorm.io/gorm" + "log" + "net/http" + "strings" + "time" +) + +func RegisterRoutes(group *gin.RouterGroup) { + group.GET("/webauthn/register/start", middleware.JWTAuth(false), beginRegistrationHandler) + group.POST("/webauthn/register/finish", middleware.JWTAuth(false), verifyRegistrationHandler) + + group.GET("/webauthn/login/start", beginLoginHandler) + group.POST("/webauthn/login/finish", middleware.RateLimiter(rate.Every(10*time.Second), 5), verifyLoginHandler) + + group.POST("/webauthn/logout", middleware.JWTAuth(false), logoutHandler) + + group.GET("/webauthn/credentials", middleware.JWTAuth(false), listCredentialsHandler) + group.PATCH("/webauthn/credentials/:id", middleware.JWTAuth(false), updateCredentialHandler) + group.DELETE("/webauthn/credentials/:id", middleware.JWTAuth(false), deleteCredentialHandler) +} + +func beginRegistrationHandler(c *gin.Context) { + var user model.User + err := common.DB.Preload("Credentials").Find(&user, "id = ?", c.GetString("userID")).Error + if err != nil { + utils.UnknownHandlerError(c, err) + log.Println(err) + return + } + + options, session, err := common.WebAuthn.BeginRegistration(&user, webauthn.WithResidentKeyRequirement(protocol.ResidentKeyRequirementRequired), webauthn.WithExclusions(user.WebAuthnCredentialDescriptors())) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + // Save the webauthn session so we can retrieve it in the verifyRegistrationHandler + sessionToStore := &model.WebauthnSession{ + ExpiresAt: session.Expires, + Challenge: session.Challenge, + UserVerification: string(session.UserVerification), + } + + if err = common.DB.Create(&sessionToStore).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.SetCookie("session_id", sessionToStore.ID, int(common.WebAuthn.Config.Timeouts.Registration.Timeout.Seconds()), "/", "", false, true) + c.JSON(http.StatusOK, options.Response) +} + +func verifyRegistrationHandler(c *gin.Context) { + sessionID, err := c.Cookie("session_id") + if err != nil { + utils.HandlerError(c, http.StatusBadRequest, "Session ID missing") + return + } + + // Retrieve the session that was previously created by the beginRegistrationHandler + var storedSession model.WebauthnSession + err = common.DB.First(&storedSession, "id = ?", sessionID).Error + + session := webauthn.SessionData{ + Challenge: storedSession.Challenge, + Expires: storedSession.ExpiresAt, + UserID: []byte(c.GetString("userID")), + } + + var user model.User + err = common.DB.Find(&user, "id = ?", c.GetString("userID")).Error + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + credential, err := common.WebAuthn.FinishRegistration(&user, session, c.Request) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + credentialToStore := model.WebauthnCredential{ + Name: "New Passkey", + CredentialID: string(credential.ID), + AttestationType: credential.AttestationType, + PublicKey: credential.PublicKey, + Transport: credential.Transport, + UserID: user.ID, + } + if err := common.DB.Create(&credentialToStore).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusOK, credentialToStore) +} + +func beginLoginHandler(c *gin.Context) { + options, session, err := common.WebAuthn.BeginDiscoverableLogin() + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + // Save the webauthn session so we can retrieve it in the verifyLoginHandler + sessionToStore := &model.WebauthnSession{ + ExpiresAt: session.Expires, + Challenge: session.Challenge, + UserVerification: string(session.UserVerification), + } + + if err = common.DB.Create(&sessionToStore).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.SetCookie("session_id", sessionToStore.ID, int(common.WebAuthn.Config.Timeouts.Registration.Timeout.Seconds()), "/", "", false, true) + c.JSON(http.StatusOK, options.Response) +} + +func verifyLoginHandler(c *gin.Context) { + sessionID, err := c.Cookie("session_id") + if err != nil { + utils.HandlerError(c, http.StatusBadRequest, "Session ID missing") + return + } + + credentialAssertionData, err := protocol.ParseCredentialRequestResponseBody(c.Request.Body) + if err != nil { + utils.HandlerError(c, http.StatusBadRequest, "Invalid body") + return + } + + // Retrieve the session that was previously created by the beginLoginHandler + var storedSession model.WebauthnSession + if err := common.DB.First(&storedSession, "id = ?", sessionID).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + session := webauthn.SessionData{ + Challenge: storedSession.Challenge, + Expires: storedSession.ExpiresAt, + } + + var user *model.User + _, err = common.WebAuthn.ValidateDiscoverableLogin(func(_, userHandle []byte) (webauthn.User, error) { + if err := common.DB.Preload("Credentials").First(&user, "id = ?", string(userHandle)).Error; err != nil { + return nil, err + } + return user, nil + }, session, credentialAssertionData) + + if err != nil { + if strings.Contains(err.Error(), gorm.ErrRecordNotFound.Error()) { + utils.HandlerError(c, http.StatusBadRequest, "no user with this passkey exists") + } else { + utils.UnknownHandlerError(c, err) + } + return + } + + err = common.DB.Find(&user, "id = ?", c.GetString("userID")).Error + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + token, err := common.GenerateAccessToken(*user) + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.SetCookie("access_token", token, int(time.Hour.Seconds()), "/", "", false, true) + c.JSON(http.StatusOK, user) +} + +func listCredentialsHandler(c *gin.Context) { + var credentials []model.WebauthnCredential + if err := common.DB.Find(&credentials, "user_id = ?", c.GetString("userID")).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusOK, credentials) +} + +func deleteCredentialHandler(c *gin.Context) { + var passkeyCount int64 + if err := common.DB.Model(&model.WebauthnCredential{}).Where("user_id = ?", c.GetString("userID")).Count(&passkeyCount).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + if passkeyCount == 1 { + utils.HandlerError(c, http.StatusBadRequest, "You must have at least one passkey") + return + } + + var credential model.WebauthnCredential + if err := common.DB.First(&credential, "id = ? AND user_id = ?", c.Param("id"), c.GetString("userID")).Error; err != nil { + utils.HandlerError(c, http.StatusNotFound, "Credential not found") + return + } + + if err := common.DB.Delete(&credential).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.Status(http.StatusNoContent) +} + +func updateCredentialHandler(c *gin.Context) { + var credential model.WebauthnCredential + if err := common.DB.Where("id = ? AND user_id = ?", c.Param("id"), c.GetString("userID")).First(&credential).Error; err != nil { + utils.HandlerError(c, http.StatusNotFound, "Credential not found") + return + } + + var input struct { + Name string `json:"name"` + } + if err := c.ShouldBindJSON(&input); err != nil { + utils.HandlerError(c, http.StatusBadRequest, "invalid request body") + return + } + + credential.Name = input.Name + + if err := common.DB.Save(&credential).Error; err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.Status(http.StatusNoContent) +} + +func logoutHandler(c *gin.Context) { + c.SetCookie("access_token", "", 0, "/", "", false, true) + c.Status(http.StatusNoContent) +} diff --git a/backend/internal/handler/well_known.go b/backend/internal/handler/well_known.go new file mode 100644 index 00000000..242cc322 --- /dev/null +++ b/backend/internal/handler/well_known.go @@ -0,0 +1,39 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/utils" + "net/http" +) + +func RegisterWellKnownRoutes(group *gin.RouterGroup) { + group.GET("/.well-known/jwks.json", jwks) + group.GET("/.well-known/openid-configuration", openIDConfiguration) +} + +func jwks(c *gin.Context) { + jwk, err := common.GetJWK() + if err != nil { + utils.UnknownHandlerError(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{"keys": []interface{}{jwk}}) +} + +func openIDConfiguration(c *gin.Context) { + appUrl := common.EnvConfig.AppURL + config := map[string]interface{}{ + "issuer": appUrl, + "authorization_endpoint": appUrl + "/authorize", + "token_endpoint": appUrl + "/api/oidc/token", + "jwks_uri": appUrl + "/.well-known/jwks.json", + "scopes_supported": []string{"openid", "profile", "email"}, + "claims_supported": []string{"sub", "given_name", "family_name", "email", "preferred_username"}, + "response_types_supported": []string{"code", "id_token"}, + "subject_types_supported": []string{"public"}, + "id_token_signing_alg_values_supported": []string{"RS256"}, + } + c.JSON(http.StatusOK, config) +} diff --git a/backend/internal/job/db_cleanup.go b/backend/internal/job/db_cleanup.go new file mode 100644 index 00000000..09b70928 --- /dev/null +++ b/backend/internal/job/db_cleanup.go @@ -0,0 +1,57 @@ +package job + +import ( + "github.com/go-co-op/gocron/v2" + "github.com/google/uuid" + "golang-rest-api-template/internal/common" + "golang-rest-api-template/internal/model" + "golang-rest-api-template/internal/utils" + "log" + "time" +) + +func RegisterJobs() { + scheduler, err := gocron.NewScheduler() + if err != nil { + log.Fatalf("Failed to create a new scheduler: %s", err) + } + + registerJob(scheduler, "ClearWebauthnSessions", "0 3 * * *", clearWebauthnSessions) + registerJob(scheduler, "ClearOneTimeAccessTokens", "0 3 * * *", clearOneTimeAccessTokens) + registerJob(scheduler, "ClearOidcAuthorizationCodes", "0 3 * * *", clearOidcAuthorizationCodes) + + scheduler.Start() +} + +func registerJob(scheduler gocron.Scheduler, name string, interval string, job func() error) { + + _, err := scheduler.NewJob( + gocron.CronJob(interval, false), + gocron.NewTask(job), + gocron.WithEventListeners( + gocron.AfterJobRuns(func(jobID uuid.UUID, jobName string) { + log.Printf("Job %q run successfully", name) + }), + gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) { + log.Printf("Job %q failed with error: %v", name, err) + }), + ), + ) + + if err != nil { + log.Fatalf("Failed to register job %q: %v", name, err) + } +} + +func clearWebauthnSessions() error { + return common.DB.Delete(&model.WebauthnSession{}, "expires_at < ?", utils.FormatDateForDb(time.Now())).Error +} + +func clearOneTimeAccessTokens() error { + return common.DB.Debug().Delete(&model.OneTimeAccessToken{}, "expires_at < ?", utils.FormatDateForDb(time.Now())).Error +} + +func clearOidcAuthorizationCodes() error { + return common.DB.Delete(&model.OidcAuthorizationCode{}, "expires_at < ?", utils.FormatDateForDb(time.Now())).Error + +} diff --git a/backend/internal/model/application_configuration.go b/backend/internal/model/application_configuration.go new file mode 100644 index 00000000..fe244e7b --- /dev/null +++ b/backend/internal/model/application_configuration.go @@ -0,0 +1,19 @@ +package model + +type ApplicationConfigurationVariable struct { + Key string `gorm:"primaryKey;not null" json:"key"` + Type string `json:"type"` + IsPublic bool `json:"-"` + IsInternal bool `json:"-"` + Value string `json:"value"` +} + +type ApplicationConfiguration struct { + AppName ApplicationConfigurationVariable + BackgroundImageType ApplicationConfigurationVariable + LogoImageType ApplicationConfigurationVariable +} + +type ApplicationConfigurationUpdateDto struct { + AppName string `json:"appName" binding:"required"` +} diff --git a/backend/internal/model/base.go b/backend/internal/model/base.go new file mode 100644 index 00000000..1fcdcdee --- /dev/null +++ b/backend/internal/model/base.go @@ -0,0 +1,20 @@ +package model + +import ( + "github.com/google/uuid" + "gorm.io/gorm" + "time" +) + +// Base contains common columns for all tables. +type Base struct { + ID string `gorm:"primaryKey;not null" json:"id"` + CreatedAt time.Time `json:"createdAt"` +} + +func (b *Base) BeforeCreate(db *gorm.DB) (err error) { + if b.ID == "" { + b.ID = uuid.New().String() + } + return +} diff --git a/backend/internal/model/oidc.go b/backend/internal/model/oidc.go new file mode 100644 index 00000000..5fd2e3bc --- /dev/null +++ b/backend/internal/model/oidc.go @@ -0,0 +1,65 @@ +package model + +import ( + "gorm.io/gorm" + "time" +) + +type UserAuthorizedOidcClient struct { + Scope string + UserID string `json:"userId" gorm:"primary_key;"` + + ClientID string `json:"clientId" gorm:"primary_key;"` + Client OidcClient +} + +type OidcClient struct { + Base + + Name string `json:"name"` + Secret string `json:"-"` + CallbackURL string `json:"callbackURL"` + ImageType *string `json:"-"` + HasLogo bool `gorm:"-" json:"hasLogo"` + + CreatedByID string + CreatedBy User +} + +func (c *OidcClient) AfterFind(_ *gorm.DB) (err error) { + // Compute HasLogo field + c.HasLogo = c.ImageType != nil && *c.ImageType != "" + return nil +} + +type OidcAuthorizationCode struct { + Base + + Code string + Scope string + Nonce string + ExpiresAt time.Time + + UserID string + User User + + ClientID string +} + +type OidcClientCreateDto struct { + Name string `json:"name" binding:"required"` + CallbackURL string `json:"callbackURL" binding:"required"` +} + +type AuthorizeNewClientDto struct { + ClientID string `json:"clientID" binding:"required"` + Scope string `json:"scope" binding:"required"` + Nonce string `json:"nonce"` +} + +type OidcIdTokenDto struct { + GrantType string `form:"grant_type" binding:"required"` + Code string `form:"code" binding:"required"` + ClientID string `form:"client_id"` + ClientSecret string `form:"client_secret"` +} diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go new file mode 100644 index 00000000..bb2867c6 --- /dev/null +++ b/backend/internal/model/user.go @@ -0,0 +1,73 @@ +package model + +import ( + "github.com/go-webauthn/webauthn/protocol" + "github.com/go-webauthn/webauthn/webauthn" + "time" +) + +type User struct { + Base + + Username string `json:"username"` + Email string `json:"email" ` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + IsAdmin bool `json:"isAdmin"` + + Credentials []WebauthnCredential `json:"-"` +} + +func (u User) WebAuthnID() []byte { return []byte(u.ID) } + +func (u User) WebAuthnName() string { return u.Username } + +func (u User) WebAuthnDisplayName() string { return u.FirstName + " " + u.LastName } + +func (u User) WebAuthnIcon() string { return "" } + +func (u User) WebAuthnCredentials() []webauthn.Credential { + credentials := make([]webauthn.Credential, len(u.Credentials)) + + for i, credential := range u.Credentials { + credentials[i] = webauthn.Credential{ + ID: []byte(credential.CredentialID), + AttestationType: credential.AttestationType, + PublicKey: credential.PublicKey, + Transport: credential.Transport, + } + + } + return credentials +} + +func (u User) WebAuthnCredentialDescriptors() (descriptors []protocol.CredentialDescriptor) { + credentials := u.WebAuthnCredentials() + + descriptors = make([]protocol.CredentialDescriptor, len(credentials)) + + for i, credential := range credentials { + descriptors[i] = credential.Descriptor() + } + + return descriptors +} + +type OneTimeAccessToken struct { + Base + Token string `json:"token"` + ExpiresAt time.Time `json:"expiresAt"` + + UserID string `json:"userId"` + User User +} + +type OneTimeAccessTokenCreateDto struct { + UserID string `json:"userId" binding:"required"` + ExpiresAt time.Time `json:"expiresAt" binding:"required"` +} + +type LoginUserDto struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} diff --git a/backend/internal/model/webauthn.go b/backend/internal/model/webauthn.go new file mode 100644 index 00000000..10f0ef93 --- /dev/null +++ b/backend/internal/model/webauthn.go @@ -0,0 +1,45 @@ +package model + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "github.com/go-webauthn/webauthn/protocol" + "time" +) + +type WebauthnSession struct { + Base + + Challenge string + ExpiresAt time.Time + UserVerification string +} + +type WebauthnCredential struct { + Base + + Name string `json:"name"` + CredentialID string `json:"credentialID"` + PublicKey []byte `json:"publicKey"` + AttestationType string `json:"attestationType"` + Transport AuthenticatorTransportList `json:"-"` + + UserID string +} + +type AuthenticatorTransportList []protocol.AuthenticatorTransport + +// Scan and Value methods for GORM to handle the custom type +func (atl *AuthenticatorTransportList) Scan(value interface{}) error { + + if v, ok := value.([]byte); ok { + return json.Unmarshal(v, atl) + } else { + return errors.New("type assertion to []byte failed") + } +} + +func (atl AuthenticatorTransportList) Value() (driver.Value, error) { + return json.Marshal(atl) +} diff --git a/backend/internal/utils/file_util.go b/backend/internal/utils/file_util.go new file mode 100644 index 00000000..f8141cd6 --- /dev/null +++ b/backend/internal/utils/file_util.go @@ -0,0 +1,73 @@ +package utils + +import ( + "io" + "os" + "path/filepath" + "strings" +) + +func GetFileExtension(filename string) string { + splitted := strings.Split(filename, ".") + return splitted[len(splitted)-1] +} + +func GetImageMimeType(ext string) string { + switch ext { + case "jpg", "jpeg": + return "image/jpeg" + case "png": + return "image/png" + case "svg": + return "image/svg+xml" + case "ico": + return "image/x-icon" + default: + return "" + } +} + +func CopyDirectory(srcDir, destDir string) error { + files, err := os.ReadDir(srcDir) + if err != nil { + return err + } + + for _, file := range files { + srcFilePath := filepath.Join(srcDir, file.Name()) + destFilePath := filepath.Join(destDir, file.Name()) + + err := copyFile(srcFilePath, destFilePath) + if err != nil { + return err + } + } + + return nil +} + +func copyFile(srcFilePath, destFilePath string) error { + srcFile, err := os.Open(srcFilePath) + if err != nil { + return err + } + defer srcFile.Close() + + err = os.MkdirAll(filepath.Dir(destFilePath), os.ModePerm) + if err != nil { + return err + } + + destFile, err := os.Create(destFilePath) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, srcFile) + if err != nil { + return err + } + + return nil +} diff --git a/backend/internal/utils/handler_error_util.go b/backend/internal/utils/handler_error_util.go new file mode 100644 index 00000000..a9206300 --- /dev/null +++ b/backend/internal/utils/handler_error_util.go @@ -0,0 +1,19 @@ +package utils + +import ( + "github.com/gin-gonic/gin" + "log" + "net/http" + "strings" +) + +func UnknownHandlerError(c *gin.Context, err error) { + log.Println(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Something went wrong"}) +} + +func HandlerError(c *gin.Context, statusCode int, message string) { + // Capitalize the first letter of the message + message = strings.ToUpper(message[:1]) + message[1:] + c.JSON(statusCode, gin.H{"error": message}) +} diff --git a/backend/internal/utils/paging_util.go b/backend/internal/utils/paging_util.go new file mode 100644 index 00000000..ecd9697c --- /dev/null +++ b/backend/internal/utils/paging_util.go @@ -0,0 +1,45 @@ +package utils + +import ( + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "strconv" +) + +type PaginationResponse struct { + TotalPages int64 `json:"totalPages"` + TotalItems int64 `json:"totalItems"` + CurrentPage int `json:"currentPage"` +} + +func Paginate(c *gin.Context, db *gorm.DB, result interface{}) (PaginationResponse, error) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) + + if page < 1 { + page = 1 + } + + if pageSize < 1 { + pageSize = 10 + } else if pageSize > 100 { + pageSize = 100 + } + + offset := (page - 1) * pageSize + + var totalItems int64 + if err := db.Count(&totalItems).Error; err != nil { + return PaginationResponse{}, err + } + + if err := db.Offset(offset).Limit(pageSize).Find(result).Error; err != nil { + return PaginationResponse{}, err + } + + return PaginationResponse{ + TotalPages: (totalItems + int64(pageSize) - 1) / int64(pageSize), + TotalItems: totalItems, + CurrentPage: page, + }, nil +} diff --git a/backend/internal/utils/string_util.go b/backend/internal/utils/string_util.go new file mode 100644 index 00000000..ff78d177 --- /dev/null +++ b/backend/internal/utils/string_util.go @@ -0,0 +1,43 @@ +package utils + +import ( + "crypto/rand" + "fmt" + "math/big" + "net/url" +) + +// GenerateRandomAlphanumericString generates a random alphanumeric string of the given length +func GenerateRandomAlphanumericString(length int) (string, error) { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + const charsetLength = int64(len(charset)) + + if length <= 0 { + return "", fmt.Errorf("length must be a positive integer") + } + + result := make([]byte, length) + + for i := range result { + num, err := rand.Int(rand.Reader, big.NewInt(charsetLength)) + if err != nil { + return "", err + } + result[i] = charset[num.Int64()] + } + + return string(result), nil +} + +func GetHostFromURL(rawURL string) string { + parsedURL, err := url.Parse(rawURL) + if err != nil { + return "" + } + return parsedURL.Host +} + +// StringPointer creates a string pointer from a string value +func StringPointer(s string) *string { + return &s +} diff --git a/backend/internal/utils/time_util.go b/backend/internal/utils/time_util.go new file mode 100644 index 00000000..18d85cde --- /dev/null +++ b/backend/internal/utils/time_util.go @@ -0,0 +1,8 @@ +package utils + +import "time" + +func FormatDateForDb(time time.Time) string { + const layout = "2006-01-02 15:04:05.000-07:00" + return time.Format(layout) +} diff --git a/backend/migrations/20240731203656_init.up.sql b/backend/migrations/20240731203656_init.up.sql new file mode 100644 index 00000000..8d4baaa0 --- /dev/null +++ b/backend/migrations/20240731203656_init.up.sql @@ -0,0 +1,80 @@ +CREATE TABLE users +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + username TEXT NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + first_name TEXT, + last_name TEXT, + is_admin NUMERIC DEFAULT FALSE NOT NULL +); + +CREATE TABLE oidc_authorization_codes +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + code TEXT NOT NULL UNIQUE, + scope TEXT NOT NULL, + nonce TEXT, + expires_at DATETIME NOT NULL, + user_id TEXT NOT NULL REFERENCES users, + client_id TEXT NOT NULL +); + +CREATE TABLE oidc_clients +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + name TEXT, + secret TEXT, + callback_url TEXT, + image_type TEXT, + created_by_id TEXT REFERENCES users +); + +CREATE TABLE one_time_access_tokens +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + token TEXT NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + user_id TEXT NOT NULL REFERENCES users +); + +CREATE TABLE user_authorized_oidc_clients +( + scope TEXT, + user_id TEXT, + client_id TEXT REFERENCES oidc_clients, + PRIMARY KEY (user_id, client_id) +); + +CREATE TABLE webauthn_credentials +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + name TEXT NOT NULL, + credential_id TEXT NOT NULL UNIQUE, + public_key BLOB NOT NULL, + attestation_type TEXT NOT NULL, + transport TEXT NOT NULL, + user_id TEXT REFERENCES users +); + +CREATE TABLE webauthn_sessions +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + challenge TEXT NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + user_verification TEXT NOT NULL +); + +CREATE TABLE application_configuration_variables +( + key TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL, + type TEXT NOT NULL, + is_public NUMERIC DEFAULT FALSE NOT NULL, + is_internal NUMERIC DEFAULT FALSE NOT NULL +); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..95d3cc8a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +services: + pocket-id: + image: pocket-id + restart: unless-stopped + env_file: .env + ports: + - 3000:80 + volumes: + - "./data:/app/backend/data" \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 00000000..a4355707 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +PUBLIC_APP_URL=http://localhost \ No newline at end of file diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 00000000..ab78a95d --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,4 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 00000000..8bc6e864 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,8 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 00000000..673e83d3 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "style": "default", + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app.css", + "baseColor": "zinc" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils/style" + }, + "typescript": true +} \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 00000000..94754801 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js'; +import ts from 'typescript-eslint'; +import svelte from 'eslint-plugin-svelte'; +import prettier from 'eslint-config-prettier'; +import globals from 'globals'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs['flat/recommended'], + prettier, + ...svelte.configs['flat/prettier'], + { + languageOptions: { + globals: { + ...globals.browser, + ...globals.node + } + } + }, + { + files: ['**/*.svelte'], + languageOptions: { + parserOptions: { + parser: ts.parser + } + } + }, + { + ignores: ['build/', '.svelte-kit/', 'dist/'] + }, + { + rules: { + "@typescript-eslint/no-explicit-any": "off" + } + } +]; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..b3862ce4 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5385 @@ +{ + "name": "pocket-id-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pocket-id-frontend", + "version": "0.0.1", + "dependencies": { + "@simplewebauthn/browser": "^10.0.0", + "axios": "^1.7.2", + "bits-ui": "^0.21.12", + "clsx": "^2.1.1", + "crypto": "^1.0.1", + "formsnap": "^1.0.1", + "jsonwebtoken": "^9.0.2", + "lucide-svelte": "^0.399.0", + "mode-watcher": "^0.4.1", + "svelte-sonner": "^0.3.27", + "sveltekit-superforms": "^2.16.1", + "tailwind-merge": "^2.3.0", + "tailwind-variants": "^0.2.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@playwright/test": "^1.46.0", + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/adapter-node": "^5.2.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@types/eslint": "^8.56.7", + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^22.1.0", + "autoprefixer": "^10.4.19", + "cbor-js": "^0.1.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.36.0", + "globals": "^15.0.0", + "postcss": "^8.4.38", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "prettier-plugin-tailwindcss": "^0.6.4", + "svelte": "^5.0.0-next.1", + "svelte-check": "^3.6.0", + "tailwindcss": "^3.4.4", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "typescript-eslint": "^8.0.0-alpha.20", + "vite": "^5.0.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ark/schema": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.2.0.tgz", + "integrity": "sha512-IkNWCSHdjaoemMXpps4uFHEAQzwJPbTAS8K2vcQpk90sa+eNBuPSVyB/81/Qyl1VYW0iX3ceGC5NL/OznQv1jg==", + "optional": true, + "dependencies": { + "@ark/util": "0.1.0" + } + }, + "node_modules/@ark/util": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.1.0.tgz", + "integrity": "sha512-qCLYICQoCy3kEKDVwirQp8qvxhY7NJd8BhhoHaj1l3wCFAk9NUbcDsxAkPStZEMdPI/d7NcbGJe8SWZuRG2twQ==", + "optional": true + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.16.0.tgz", + "integrity": "sha512-/jmuSd74i4Czf1XXn7wGRWZCuyaUZ330NH1Bek0Pplatt4Sy1S5haN21SCLLdbeKslQ+S0wEJ+++v5YibSi+Lg==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", + "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "optional": true + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==" + }, + "node_modules/@gcornut/valibot-json-schema": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@gcornut/valibot-json-schema/-/valibot-json-schema-0.31.0.tgz", + "integrity": "sha512-3xGptCurm23e7nuPQkdrE5rEs1FeTPHhAUsBuwwqG4/YeZLwJOoYZv+fmsppUEfo5y9lzUwNQrNqLS/q7HMc7g==", + "optional": true, + "dependencies": { + "valibot": "~0.31.0" + }, + "bin": { + "valibot-json-schema": "bin/index.js" + }, + "optionalDependencies": { + "@types/json-schema": ">= 7.0.14", + "esbuild": ">= 0.18.20", + "esbuild-runner": ">= 2.2.2" + } + }, + "node_modules/@gcornut/valibot-json-schema/node_modules/valibot": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.31.1.tgz", + "integrity": "sha512-2YYIhPrnVSz/gfT2/iXVTrSj92HwchCt9Cga/6hX4B26iCz9zkIsGTS0HjDYTZfTi1Un0X6aRvhBi1cfqs/i0Q==", + "optional": true + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "optional": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@internationalized/date": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.4.tgz", + "integrity": "sha512-qoVJVro+O0rBaw+8HPjUB1iH8Ihf8oziEnqMnvhJUSuVIrHOuZ6eNLHNvzXJKUvAtaDiqMnRlg8Z2mgh09BlUw==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", + "dev": true, + "dependencies": { + "playwright": "1.46.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.25", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==" + }, + "node_modules/@poppinss/macroable": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@poppinss/macroable/-/macroable-1.0.2.tgz", + "integrity": "sha512-xhhEcEvhQC8mP5oOr5hbE4CmUgmw/IPV1jhpGg2xSkzoFrt9i8YVqBQt9744EFesi5F7pBheWozg63RUBM/5JA==", + "optional": true, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz", + "integrity": "sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^10.4.1", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", + "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", + "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", + "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", + "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", + "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", + "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", + "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", + "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", + "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", + "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", + "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", + "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", + "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", + "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", + "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", + "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "optional": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "optional": true + }, + "node_modules/@simplewebauthn/browser": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-10.0.0.tgz", + "integrity": "sha512-hG0JMZD+LiLUbpQcAjS4d+t4gbprE/dLYop/CkE01ugU/9sKXflxV5s0DRjdz3uNMFecatRfb4ZLG3XvF8m5zg==", + "dependencies": { + "@simplewebauthn/types": "^10.0.0" + } + }, + "node_modules/@simplewebauthn/types": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/types/-/types-10.0.0.tgz", + "integrity": "sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.32.35", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.32.35.tgz", + "integrity": "sha512-Ul3YyOTU++to8cgNkttakC0dWvpERr6RYoHO2W47DLbFvrwBDJUY31B1sImH6JZSYc4Kt4PyHtoPNu+vL2r2dA==", + "optional": true + }, + "node_modules/@sodaru/yup-to-json-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@sodaru/yup-to-json-schema/-/yup-to-json-schema-2.0.1.tgz", + "integrity": "sha512-lWb0Wiz8KZ9ip/dY1eUqt7fhTPmL24p6Hmv5Fd9pzlzAdw/YNcWZr+tiCT4oZ4Zyxzi9+1X4zv82o7jYvcFxYA==", + "optional": true + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.2.2.tgz", + "integrity": "sha512-Mso5xPCA8zgcKrv+QioVlqMZkyUQ5MjDJiEPuG/Z7cV/5tmwV7LmcVWk5tZ+H0NCOV1x12AsoSpt/CwFwuVXMA==", + "dev": true, + "dependencies": { + "import-meta-resolve": "^4.1.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.0.tgz", + "integrity": "sha512-HVZoei2078XSyPmvdTHE03VXDUD0ytTvMuMHMQP0j6zX4nPDpCcKrgvU7baEblMeCCMdM/shQvstFxOJPQKlUQ==", + "dev": true, + "dependencies": { + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.5.17", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.17.tgz", + "integrity": "sha512-wiADwq7VreR3ctOyxilAZOfPz3Jiy2IIp2C8gfafhTdQaVuGIHllfqQm8dXZKADymKr3uShxzgLZFT+a+CM4kA==", + "hasInstallScript": true, + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^5.0.0", + "esm-env": "^1.0.0", + "import-meta-resolve": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^2.0.4", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz", + "integrity": "sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.10", + "svelte-hmr": "^0.16.0", + "vitefu": "^0.2.5" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", + "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte/node_modules/svelte-hmr": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", + "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "devOptional": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "devOptional": true, + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true + }, + "node_modules/@types/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==", + "optional": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0-alpha.30.tgz", + "integrity": "sha512-2CBUupdkfbE3eATph4QeZejvT+M+1bVur+zXlVx09WN31phap51ps/qemeclnCbGEz6kTgBDmScrr9XmmF8/Pg==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.0.0-alpha.30", + "@typescript-eslint/type-utils": "8.0.0-alpha.30", + "@typescript-eslint/utils": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.0-alpha.30.tgz", + "integrity": "sha512-tAYgFmgXU1MlCK3nbblUvJlDSibBvxtAQXGrF3IG0KmnRza9FXILZifHWL0rrwacDn40K53K607Fk2QkMjiGgw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.0.0-alpha.30", + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.30.tgz", + "integrity": "sha512-FGW/iPWGyPFamAVZ60oCAthMqQrqafUGebF8UKuq/ha+e9SVG6YhJoRzurlQXOVf8dHfOhJ0ADMXyFnMc53clg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.0-alpha.30.tgz", + "integrity": "sha512-FrnhlCKEKZKRbpDviHkIU9tayIUGTOfa+SjvrRv6p/AJIUv6QT8oRboRjLH/cCuwUEbM0k5UtRWYug4albHUqQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.0.0-alpha.30", + "@typescript-eslint/utils": "8.0.0-alpha.30", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.0-alpha.30.tgz", + "integrity": "sha512-4WzLlw27SO9pK9UFj/Hu7WGo8WveT0SEiIpFVsV2WwtQmLps6kouwtVCB8GJPZKJyurhZhcqCoQVQFmpv441Vg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.30.tgz", + "integrity": "sha512-WSXbc9ZcXI+7yC+6q95u77i8FXz6HOLsw3ST+vMUlFy1lFbXyFL/3e6HDKQCm2Clt0krnoCPiTGvIn+GkYPn4Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.30", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.0-alpha.30.tgz", + "integrity": "sha512-rfhqfLqFyXhHNDwMnHiVGxl/Z2q/3guQ1jLlGQ0hi9Rb7inmwz42crM+NnLPR+2vEnwyw1P/g7fnQgQ3qvFx4g==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.0.0-alpha.30", + "@typescript-eslint/types": "8.0.0-alpha.30", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.30" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.30.tgz", + "integrity": "sha512-XZuNurZxBqmr6ZIRIwWFq7j5RZd6ZlkId/HZEWyfciK+CWoyOxSF9Pv2VXH9Rlu2ZG2PfbhLz2Veszl4Pfn7yA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.0.0-alpha.30", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vinejs/compiler": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@vinejs/compiler/-/compiler-2.5.0.tgz", + "integrity": "sha512-hg4ekaB5Y2zh+IWzBiC/WCDWrIfpVnKu/ubUvelKlidc/VbulsexoFRw5kJGHZenPVI5YzNnDeTdYSALkTV7jQ==", + "optional": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@vinejs/vine": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@vinejs/vine/-/vine-1.8.0.tgz", + "integrity": "sha512-Qq3XxbA26jzqS9ICifkqzT399lMQZ2fWtqeV3luI2as+UIK7qDifJFU2Q4W3q3IB5VXoWxgwAZSZEO0em9I/qQ==", + "optional": true, + "dependencies": { + "@poppinss/macroable": "^1.0.1", + "@types/validator": "^13.11.9", + "@vinejs/compiler": "^2.4.1", + "camelcase": "^8.0.0", + "dayjs": "^1.11.10", + "dlv": "^1.1.3", + "normalize-url": "^8.0.1", + "validator": "^13.11.0" + }, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-typescript": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz", + "integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==", + "peerDependencies": { + "acorn": ">=8.9.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/arktype": { + "version": "2.0.0-beta.0", + "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.0.0-beta.0.tgz", + "integrity": "sha512-fE3ssMiXjr/bLqFPzlDhRlXngdyHQreu7p7i8+dtcY1CA+f8WrVUcue6JxywhnqEJXPG4HOcIwQcC+q4VfeUMQ==", + "optional": true, + "dependencies": { + "@ark/schema": "0.2.0", + "@ark/util": "0.1.0" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bits-ui": { + "version": "0.21.12", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.21.12.tgz", + "integrity": "sha512-Cf0iB+ZKwA0ZjkpixrhrZK9PC6pGPFleW/65Xc/z0lpGvWaFtdOhiYEntCHHxZ0VihP3aJaG0OBhUBIbmAePaA==", + "dependencies": { + "@internationalized/date": "^3.5.1", + "@melt-ui/svelte": "0.76.2", + "nanoid": "^5.0.5" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.118" + } + }, + "node_modules/bits-ui/node_modules/@melt-ui/svelte": { + "version": "0.76.2", + "resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.76.2.tgz", + "integrity": "sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA==", + "dependencies": { + "@floating-ui/core": "^1.3.1", + "@floating-ui/dom": "^1.4.5", + "@internationalized/date": "^3.5.0", + "dequal": "^2.0.3", + "focus-trap": "^7.5.2", + "nanoid": "^5.0.4" + }, + "peerDependencies": { + "svelte": ">=3 <5" + } + }, + "node_modules/bits-ui/node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "optional": true + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "optional": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001636", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", + "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/cbor-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/cbor-js/-/cbor-js-0.1.0.tgz", + "integrity": "sha512-7sQ/TvDZPl7csT1Sif9G0+MA0I0JOVah8+wWlJVQdVEgIbCzlN/ab3x+uvMNsc34TUvO6osQTAmB2ls80JX6tw==", + "dev": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dayjs": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", + "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==", + "optional": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.0.0.tgz", + "integrity": "sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.810", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.810.tgz", + "integrity": "sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/esbuild-runner": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/esbuild-runner/-/esbuild-runner-2.2.2.tgz", + "integrity": "sha512-fRFVXcmYVmSmtYm2mL8RlUASt2TDkGh3uRcvHFOKNr/T58VrfVeKD9uT9nlgxk96u0LS0ehS/GY7Da/bXWKkhw==", + "optional": true, + "dependencies": { + "source-map-support": "0.5.21", + "tslib": "2.4.0" + }, + "bin": { + "esr": "bin/esr.js" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/esbuild-runner/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "optional": true + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.5.0.tgz", + "integrity": "sha512-+NAOZFrW/jFTS3dASCGBxX1pkFD0/fsO+hfAkJ4TyYKwgsXZbqzrw+seCYFCcPCYXvnD67tAnglU7GQTz6kcVw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/config-array": "^0.16.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.5.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.40.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.40.0.tgz", + "integrity": "sha512-JuOzmfVaMeEkBASL7smHu3tIU4D9rWkHuRNV+zm/5zgAwiZVvxrXM7TcfIOS+U7VXOr4uCZuE+kZTVTzS0IE+Q==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@jridgewell/sourcemap-codec": "^1.4.15", + "eslint-compat-utils": "^0.5.1", + "esutils": "^2.0.3", + "known-css-properties": "^0.32.0", + "postcss": "^8.4.38", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.1.0", + "semver": "^7.6.2", + "svelte-eslint-parser": "^0.39.1" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.155" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==" + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz", + "integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formsnap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/formsnap/-/formsnap-1.0.1.tgz", + "integrity": "sha512-TvU9CoLSiacW1c7wXhLiyVpyy/LBfG0CEFDbs3M3jrsxBSrkTpsuhbQ8JYKY3CNCmIhZlgxCH+Vqr7RBF9G53w==", + "dependencies": { + "nanoid": "^5.0.5" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.1", + "sveltekit-superforms": "^2.3.0" + } + }, + "node_modules/formsnap/node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.6.0.tgz", + "integrity": "sha512-UzcJi88Hw//CurUIRa9Jxb0vgOCcuD/MNjwmXp633cyaRKkCWACkoqHCtfZv43b1kqXGg/fpOa8bwgacCeXsVg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-core-module": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.0.tgz", + "integrity": "sha512-UeVN/ery4/JeXI8h4rM8yZPxsH+KqPi/84qFxHfTGHZnWnK9D0UU9ZGYO+6XAaJLqCWMiks+ARuFOKAiSxJCHA==", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/just-clone": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-clone/-/just-clone-6.2.0.tgz", + "integrity": "sha512-1IynUYEc/HAwxhi3WDpIpxJbZpMCvvrrmZVqvj9EhpvbH8lls7HhdhiByjL7DkAaWlLIzpC0Xc/VPvy/UxLNjA==" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.32.0.tgz", + "integrity": "sha512-PXuex21brpp7qENI143ZL5cWQcMR4IZVeeZv9ew6dg+bZX2xRUu/NzGKudZJY5DO4APiMkNPYIF8VGIdY08Tdw==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/lucide-svelte": { + "version": "0.399.0", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.399.0.tgz", + "integrity": "sha512-NQ8AxNMKbIJsx7HV//gnAsIY1wJfb3rbXSK2S/ZDjIldvAEdzGngpUT8T8Q8zHYUuii0bavAmVARN8giR4vvpA==", + "peerDependencies": { + "svelte": "^3 || ^4 || ^5.0.0-next.42" + } + }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/memoize-weak": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/memoize-weak/-/memoize-weak-1.0.2.tgz", + "integrity": "sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mode-watcher": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-0.4.1.tgz", + "integrity": "sha512-bNC+1NXmwEFZtziCdZSgP7HFQTpqJPcQn9GwwJQGSf6SBF3neEPYV1uRwkYuAQwbsvsXIYtzaqgedDzJ7D1mhg==", + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.1" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "optional": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", + "dev": true, + "dependencies": { + "playwright-core": "1.46.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.5.tgz", + "integrity": "sha512-vP/M/Goc8z4iVIvrwXwbrYVjJgA0Hf8PO1G4LBh/ocSt6vUP6sLvyu9F3ABEGr+dbKyxZjEKLkeFsWy/yYl0HQ==", + "dev": true, + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz", + "integrity": "sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==", + "dev": true, + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig-melody": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig-melody": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "optional": true + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", + "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sorcery": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz", + "integrity": "sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^1.0.0", + "minimist": "^1.2.0", + "sander": "^0.5.0" + }, + "bin": { + "sorcery": "bin/sorcery" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "optional": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.0.0-next.164", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.0.0-next.164.tgz", + "integrity": "sha512-e+V1dSZLCCu0Ln7RKwSl/zP9ZZFbDGW7ABfGQYU/+6sWponizQocmyHyKHzctor0ruGgJKOF7QQB+M6lE2+UrA==", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.5", + "acorn": "^8.11.3", + "acorn-typescript": "^1.4.13", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "esm-env": "^1.0.0", + "esrap": "^1.2.2", + "is-reference": "^3.0.2", + "locate-character": "^3.0.0", + "magic-string": "^0.30.5", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.1.tgz", + "integrity": "sha512-KlQ0TRVe01mdvh49Ylkr9FQxO/UWbQOtaIrccl3gjgkvby1TxY41VkT7ijCl6i29FjaJPE4m6YGmhdqov0MfkA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "chokidar": "^3.4.1", + "fast-glob": "^3.2.7", + "import-fresh": "^3.2.1", + "picocolors": "^1.0.0", + "sade": "^1.7.4", + "svelte-preprocess": "^5.1.3", + "typescript": "^5.0.3" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "peerDependencies": { + "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.39.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.39.1.tgz", + "integrity": "sha512-0VR9gq2TOdSrJW94Qf2F3XrzXRQomXQtRZGFS3FEUr3G4J8DcpqXfBF1HJyOa3dACyGsKiBbOPF56pBgYaqXBA==", + "dev": true, + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "postcss": "^8.4.38", + "postcss-scss": "^4.0.9" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.115" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-preprocess": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz", + "integrity": "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.30.5", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte-sonner": { + "version": "0.3.27", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.27.tgz", + "integrity": "sha512-+PvbuePNTyTNCepCWqKcFeu+Lo27yYuwfSc7zJvrWjCRMJrWAmccPg3j7jO1W13QoN3TySXU5Trb956VBQiM5Q==", + "peerDependencies": { + "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1" + } + }, + "node_modules/sveltekit-superforms": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/sveltekit-superforms/-/sveltekit-superforms-2.16.1.tgz", + "integrity": "sha512-RNBdN43xge/ADmc3s7+pfdnRGuZ9gZiqpX6VKAQCnCI+ICc5rrPv5idYbx4iuY1Ia0lRMAq1hP0x2oHaPjB+Kg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ciscoheat" + }, + { + "type": "ko-fi", + "url": "https://ko-fi.com/ciscoheat" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=NY7F5ALHHSVQS" + } + ], + "dependencies": { + "devalue": "^5.0.0", + "just-clone": "^6.2.0", + "memoize-weak": "^1.0.2", + "ts-deepmerge": "^7.0.0" + }, + "optionalDependencies": { + "@exodus/schemasafe": "^1.3.0", + "@gcornut/valibot-json-schema": "^0.31.0", + "@sinclair/typebox": "^0.32.34", + "@sodaru/yup-to-json-schema": "^2.0.1", + "@vinejs/vine": "^1.8.0", + "arktype": "2.0.0-beta.0", + "joi": "^17.13.3", + "json-schema-to-ts": "^3.1.0", + "superstruct": "^2.0.2", + "valibot": "^0.35.0", + "yup": "^1.4.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.23.1" + }, + "peerDependencies": { + "@exodus/schemasafe": "^1.3.0", + "@sinclair/typebox": ">=0.32.30 <1", + "@sveltejs/kit": "1.x || 2.x", + "@vinejs/vine": "^1.8.0", + "arktype": ">=2.0.0-beta.0", + "joi": "^17.13.1", + "superstruct": "^2.0.2", + "svelte": "3.x || 4.x || >=5.0.0-next.51", + "valibot": ">=0.33.0 <1", + "yup": "^1.4.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "@exodus/schemasafe": { + "optional": true + }, + "@sinclair/typebox": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "arktype": { + "optional": true + }, + "joi": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "valibot": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, + "node_modules/tailwind-merge": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz", + "integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==", + "dependencies": { + "@babel/runtime": "^7.24.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-0.2.1.tgz", + "integrity": "sha512-2xmhAf4UIc3PijOUcJPA1LP4AbxhpcHuHM2C26xM0k81r0maAO6uoUSHl3APmvHZcY5cZCY/bYuJdfFa4eGoaw==", + "dependencies": { + "tailwind-merge": "^2.2.0" + }, + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwindcss": "*" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "optional": true + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "optional": true + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "optional": true + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-deepmerge": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.1.tgz", + "integrity": "sha512-JBFCmNenZdUCc+TRNCtXVM6N8y/nDQHAcpj5BlwXG/gnogjam1NunulB9ia68mnqYI446giMfpqeBFFkOleh+g==", + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "optional": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", + "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.0.0-alpha.30", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.0.0-alpha.30.tgz", + "integrity": "sha512-/vGhBMsK1TpadQh1eQ02c5pyiPGmKR9cVzX5C9plZ+LC0HPLpWoJbbTVfQN7BkIK7tUxDt2BFr3pFL5hDDrx7g==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.0.0-alpha.30", + "@typescript-eslint/parser": "8.0.0-alpha.30", + "@typescript-eslint/utils": "8.0.0-alpha.30" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==", + "devOptional": true + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/valibot": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.35.0.tgz", + "integrity": "sha512-+i2aCRkReTrd5KBN/dW2BrPOvFnU5LXTV2xjZnjnqUIO8YUx6P2+MgRrkwF2FhkexgyKq/NIZdPdknhHf5A/Ww==", + "optional": true + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "optional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", + "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yup": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", + "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "optional": true, + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.1.tgz", + "integrity": "sha512-oT9INvydob1XV0v1d2IadrR74rLtDInLvDFfAa1CG0Pmg/vxATk7I2gSelfj271mbzeM4Da0uuDQE/Nkj3DWNw==", + "optional": true, + "peerDependencies": { + "zod": "^3.23.3" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..8113f2fc --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,58 @@ +{ + "name": "pocket-id-frontend", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev --port 3000", + "build": "vite build", + "preview": "vite preview --port 3000", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --check . && eslint .", + "format": "prettier --write ." + }, + "devDependencies": { + "@playwright/test": "^1.46.0", + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/adapter-node": "^5.2.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@types/eslint": "^8.56.7", + "@types/jsonwebtoken": "^9.0.6", + "@types/node": "^22.1.0", + "autoprefixer": "^10.4.19", + "cbor-js": "^0.1.0", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.36.0", + "globals": "^15.0.0", + "postcss": "^8.4.38", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "prettier-plugin-tailwindcss": "^0.6.4", + "svelte": "^5.0.0-next.1", + "svelte-check": "^3.6.0", + "tailwindcss": "^3.4.4", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "typescript-eslint": "^8.0.0-alpha.20", + "vite": "^5.0.3" + }, + "type": "module", + "dependencies": { + "@simplewebauthn/browser": "^10.0.0", + "axios": "^1.7.2", + "bits-ui": "^0.21.12", + "clsx": "^2.1.1", + "crypto": "^1.0.1", + "formsnap": "^1.0.1", + "jsonwebtoken": "^9.0.2", + "lucide-svelte": "^0.399.0", + "mode-watcher": "^0.4.1", + "svelte-sonner": "^0.3.27", + "sveltekit-superforms": "^2.16.1", + "tailwind-merge": "^2.3.0", + "tailwind-variants": "^0.2.1", + "zod": "^3.23.8" + } +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 00000000..e4005180 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + outputDir: './tests/.output', + timeout: 10000, + testDir: './tests', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: process.env.CI + ? [['html'], ['github']] + : [['line'], ['html', { open: 'never', outputFolder: 'tests/.output' }]], + use: { + baseURL: 'http://localhost', + video: 'retain-on-failure', + trace: 'on-first-retry' + }, + projects: [ + { name: 'setup', testMatch: /.*\.setup\.ts/ }, + { + name: 'chromium', + use: { ...devices['Desktop Chrome'], storageState: 'tests/.auth/user.json' }, + dependencies: ['setup'] + } + ] +}); diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 00000000..0f772168 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 00000000..ad29ad56 --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,112 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + + --destructive: 0 72.2% 50.6%; + --destructive-foreground: 0 0% 98%; + + --ring: 240 10% 3.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + + --ring: 240 4.9% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + + body { + @apply bg-background text-foreground; + } + + @font-face { + font-family: 'Playfair Display'; + font-weight: 400; + src: url('/fonts/PlayfairDisplay-Regular.woff') format('woff'); + } + @font-face { + font-family: 'Playfair Display'; + font-weight: 500; + src: url('/fonts/PlayfairDisplay-Medium.woff') format('woff'); + } + @font-face { + font-family: 'Playfair Display'; + font-weight: 600; + src: url('/fonts/PlayfairDisplay-SemiBold.woff') format('woff'); + } + @font-face { + font-family: 'Playfair Display'; + font-weight: 700; + src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff'); + } +} +@layer components { + .application-images-grid { + @apply flex flex-wrap justify-between gap-x-5 gap-y-8; + } + + @media (max-width: 1127px) { + .application-images-grid { + justify-content: flex-start; + @apply gap-x-20; + } + } +} diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts new file mode 100644 index 00000000..342b5eba --- /dev/null +++ b/frontend/src/app.d.ts @@ -0,0 +1,16 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + interface Error { + message: string; + status?: number; + } + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html new file mode 100644 index 00000000..215bf39c --- /dev/null +++ b/frontend/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts new file mode 100644 index 00000000..ce645aa2 --- /dev/null +++ b/frontend/src/hooks.server.ts @@ -0,0 +1,61 @@ +import type { Handle, HandleServerError } from '@sveltejs/kit'; +import { AxiosError } from 'axios'; +import jwt from 'jsonwebtoken'; + +export const handle: Handle = async ({ event, resolve }) => { + const accessToken = event.cookies.get('access_token'); + + let isSignedIn: boolean = false; + let isAdmin: boolean = false; + + if (accessToken) { + const jwtPayload = jwt.decode(accessToken, { json: true }); + if (jwtPayload?.exp && jwtPayload.exp * 1000 > Date.now()) { + isSignedIn = true; + isAdmin = jwtPayload?.isAdmin || false; + } + } + + if (event.url.pathname.startsWith('/settings') && !event.url.pathname.startsWith('/login')) { + if (!isSignedIn) { + return new Response(null, { + status: 302, + headers: { location: '/login' } + }); + } + } + + if (event.url.pathname.startsWith('/login') && isSignedIn) { + return new Response(null, { + status: 302, + headers: { location: '/settings' } + }); + } + + if (event.url.pathname.startsWith('/settings/admin') && !isAdmin) { + return new Response(null, { + status: 302, + headers: { location: '/settings' } + }); + } + + const response = await resolve(event); + return response; +}; + +export const handleError: HandleServerError = async ({ error, message, status }) => { + if (error instanceof AxiosError) { + message = error.response?.data.error || message; + status = error.response?.status || status; + console.error( + `Axios error: ${error.request.path} - ${error.response?.data.error ?? error.message}` + ); + } else { + console.error(error); + } + + return { + message, + status + }; +}; diff --git a/frontend/src/lib/components/confirm-dialog/confirm-dialog.svelte b/frontend/src/lib/components/confirm-dialog/confirm-dialog.svelte new file mode 100644 index 00000000..b2340808 --- /dev/null +++ b/frontend/src/lib/components/confirm-dialog/confirm-dialog.svelte @@ -0,0 +1,30 @@ + + + + + + {$confirmDialogStore.title} + + {$confirmDialogStore.message} + + + + Cancel + + + + + + diff --git a/frontend/src/lib/components/confirm-dialog/index.ts b/frontend/src/lib/components/confirm-dialog/index.ts new file mode 100644 index 00000000..8812a609 --- /dev/null +++ b/frontend/src/lib/components/confirm-dialog/index.ts @@ -0,0 +1,39 @@ +import { writable } from 'svelte/store'; +import ConfirmDialog from './confirm-dialog.svelte'; + +export const confirmDialogStore = writable({ + open: false, + title: '', + message: '', + confirm: { + label: 'Confirm', + destructive: false, + action: () => {} + } +}); + +function openConfirmDialog({ + title, + message, + confirm +}: { + title: string; + message: string; + confirm: { + label?: string; + destructive?: boolean; + action: () => void; + }; +}) { + confirmDialogStore.update((val) => ({ + open: true, + title, + message, + confirm: { + ...val.confirm, + ...confirm + } + })); +} + +export { ConfirmDialog, openConfirmDialog }; diff --git a/frontend/src/lib/components/error.svelte b/frontend/src/lib/components/error.svelte new file mode 100644 index 00000000..c4581b09 --- /dev/null +++ b/frontend/src/lib/components/error.svelte @@ -0,0 +1,15 @@ + + +
+ +

Something went wrong

+

{message}

+ {#if showButton} + + {/if} +
diff --git a/frontend/src/lib/components/file-input.svelte b/frontend/src/lib/components/file-input.svelte new file mode 100644 index 00000000..26dbbf60 --- /dev/null +++ b/frontend/src/lib/components/file-input.svelte @@ -0,0 +1,27 @@ + + + + diff --git a/frontend/src/lib/components/form-input.svelte b/frontend/src/lib/components/form-input.svelte new file mode 100644 index 00000000..a285288e --- /dev/null +++ b/frontend/src/lib/components/form-input.svelte @@ -0,0 +1,30 @@ + + +
+ + {#if children} + {@render children()} + {:else} + + {/if} + {#if input.error} +

{input.error}

+ {/if} +
diff --git a/frontend/src/lib/components/header/header-avatar.svelte b/frontend/src/lib/components/header/header-avatar.svelte new file mode 100644 index 00000000..11c55a14 --- /dev/null +++ b/frontend/src/lib/components/header/header-avatar.svelte @@ -0,0 +1,47 @@ + + + + + {initials} + + + +
+

+ {$userStore?.firstName} + {$userStore?.lastName} +

+

{$userStore?.email}

+
+
+ + + My Account + Logout + +
+
diff --git a/frontend/src/lib/components/header/header.svelte b/frontend/src/lib/components/header/header.svelte new file mode 100644 index 00000000..c0311cf6 --- /dev/null +++ b/frontend/src/lib/components/header/header.svelte @@ -0,0 +1,27 @@ + + +
+
+
+ {#if !isAuthPage} + +

+ {$applicationConfigurationStore.appName} +

+ {/if} +
+ {#if $userStore?.id} + + {/if} +
+
diff --git a/frontend/src/lib/components/login-wrapper.svelte b/frontend/src/lib/components/login-wrapper.svelte new file mode 100644 index 00000000..e9a68e40 --- /dev/null +++ b/frontend/src/lib/components/login-wrapper.svelte @@ -0,0 +1,42 @@ + + + + +
+ + + {#if browser && !browserSupportsWebAuthn()} + + {:else} + {@render children()} + {/if} + + +
diff --git a/frontend/src/lib/components/logo.svelte b/frontend/src/lib/components/logo.svelte new file mode 100644 index 00000000..bce3dae2 --- /dev/null +++ b/frontend/src/lib/components/logo.svelte @@ -0,0 +1 @@ +Logo diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte new file mode 100644 index 00000000..3fdf67a9 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-action.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte new file mode 100644 index 00000000..c346110b --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-cancel.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte new file mode 100644 index 00000000..bcfb5e50 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-content.svelte @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte new file mode 100644 index 00000000..86d07064 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-description.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte new file mode 100644 index 00000000..f1182404 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-footer.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte new file mode 100644 index 00000000..19ed2c74 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-header.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte new file mode 100644 index 00000000..c785fe7c --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-overlay.svelte @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte new file mode 100644 index 00000000..e2272192 --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-portal.svelte @@ -0,0 +1,9 @@ + + + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte new file mode 100644 index 00000000..017e6e6e --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/alert-dialog-title.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/frontend/src/lib/components/ui/alert-dialog/index.ts b/frontend/src/lib/components/ui/alert-dialog/index.ts new file mode 100644 index 00000000..be56dd7b --- /dev/null +++ b/frontend/src/lib/components/ui/alert-dialog/index.ts @@ -0,0 +1,40 @@ +import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; + +import Title from "./alert-dialog-title.svelte"; +import Action from "./alert-dialog-action.svelte"; +import Cancel from "./alert-dialog-cancel.svelte"; +import Portal from "./alert-dialog-portal.svelte"; +import Footer from "./alert-dialog-footer.svelte"; +import Header from "./alert-dialog-header.svelte"; +import Overlay from "./alert-dialog-overlay.svelte"; +import Content from "./alert-dialog-content.svelte"; +import Description from "./alert-dialog-description.svelte"; + +const Root = AlertDialogPrimitive.Root; +const Trigger = AlertDialogPrimitive.Trigger; + +export { + Root, + Title, + Action, + Cancel, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + // + Root as AlertDialog, + Title as AlertDialogTitle, + Action as AlertDialogAction, + Cancel as AlertDialogCancel, + Portal as AlertDialogPortal, + Footer as AlertDialogFooter, + Header as AlertDialogHeader, + Trigger as AlertDialogTrigger, + Overlay as AlertDialogOverlay, + Content as AlertDialogContent, + Description as AlertDialogDescription, +}; diff --git a/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte b/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 00000000..0bb42b1b --- /dev/null +++ b/frontend/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/frontend/src/lib/components/ui/avatar/avatar-image.svelte b/frontend/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 00000000..0c92f19f --- /dev/null +++ b/frontend/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,18 @@ + + + diff --git a/frontend/src/lib/components/ui/avatar/avatar.svelte b/frontend/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 00000000..17147587 --- /dev/null +++ b/frontend/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/frontend/src/lib/components/ui/avatar/index.ts b/frontend/src/lib/components/ui/avatar/index.ts new file mode 100644 index 00000000..d06457be --- /dev/null +++ b/frontend/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,13 @@ +import Root from "./avatar.svelte"; +import Image from "./avatar-image.svelte"; +import Fallback from "./avatar-fallback.svelte"; + +export { + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, +}; diff --git a/frontend/src/lib/components/ui/badge/badge.svelte b/frontend/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 00000000..70c5a052 --- /dev/null +++ b/frontend/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/frontend/src/lib/components/ui/badge/index.ts b/frontend/src/lib/components/ui/badge/index.ts new file mode 100644 index 00000000..e9079937 --- /dev/null +++ b/frontend/src/lib/components/ui/badge/index.ts @@ -0,0 +1,21 @@ +import { type VariantProps, tv } from "tailwind-variants"; +export { default as Badge } from "./badge.svelte"; + +export const badgeVariants = tv({ + base: "inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +export type Variant = VariantProps["variant"]; diff --git a/frontend/src/lib/components/ui/button/button.svelte b/frontend/src/lib/components/ui/button/button.svelte new file mode 100644 index 00000000..4ec6f6d2 --- /dev/null +++ b/frontend/src/lib/components/ui/button/button.svelte @@ -0,0 +1,32 @@ + + + + {#if isLoading} + + {/if} + + diff --git a/frontend/src/lib/components/ui/button/index.ts b/frontend/src/lib/components/ui/button/index.ts new file mode 100644 index 00000000..84bf7df8 --- /dev/null +++ b/frontend/src/lib/components/ui/button/index.ts @@ -0,0 +1,50 @@ +import { type VariantProps, tv } from "tailwind-variants"; +import type { Button as ButtonPrimitive } from "bits-ui"; +import Root from "./button.svelte"; + +const buttonVariants = tv({ + base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, +}); + +type Variant = VariantProps["variant"]; +type Size = VariantProps["size"]; + +type Props = ButtonPrimitive.Props & { + variant?: Variant; + size?: Size; + isLoading?: boolean; +}; + +type Events = ButtonPrimitive.Events; + +export { + Root, + type Props, + type Events, + // + Root as Button, + type Props as ButtonProps, + type Events as ButtonEvents, + buttonVariants, +}; diff --git a/frontend/src/lib/components/ui/card/card-content.svelte b/frontend/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 00000000..ff615be2 --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/frontend/src/lib/components/ui/card/card-description.svelte b/frontend/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 00000000..d56070dd --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,13 @@ + + +

+ +

diff --git a/frontend/src/lib/components/ui/card/card-footer.svelte b/frontend/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 00000000..f0b3af1d --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/frontend/src/lib/components/ui/card/card-header.svelte b/frontend/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 00000000..8740b36b --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/frontend/src/lib/components/ui/card/card-title.svelte b/frontend/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 00000000..fbc3c76d --- /dev/null +++ b/frontend/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/frontend/src/lib/components/ui/card/card.svelte b/frontend/src/lib/components/ui/card/card.svelte new file mode 100644 index 00000000..5cfd46c2 --- /dev/null +++ b/frontend/src/lib/components/ui/card/card.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/frontend/src/lib/components/ui/card/index.ts b/frontend/src/lib/components/ui/card/index.ts new file mode 100644 index 00000000..bcc031d0 --- /dev/null +++ b/frontend/src/lib/components/ui/card/index.ts @@ -0,0 +1,24 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, +}; + +export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; diff --git a/frontend/src/lib/components/ui/checkbox/checkbox.svelte b/frontend/src/lib/components/ui/checkbox/checkbox.svelte new file mode 100644 index 00000000..ce76896a --- /dev/null +++ b/frontend/src/lib/components/ui/checkbox/checkbox.svelte @@ -0,0 +1,35 @@ + + + + + {#if isChecked} + + {:else if isIndeterminate} + + {/if} + + diff --git a/frontend/src/lib/components/ui/checkbox/index.ts b/frontend/src/lib/components/ui/checkbox/index.ts new file mode 100644 index 00000000..6d92d945 --- /dev/null +++ b/frontend/src/lib/components/ui/checkbox/index.ts @@ -0,0 +1,6 @@ +import Root from "./checkbox.svelte"; +export { + Root, + // + Root as Checkbox, +}; diff --git a/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/frontend/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 00000000..31d8f178 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,36 @@ + + + + + + + + + Close + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-description.svelte b/frontend/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 00000000..df097db7 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-footer.svelte b/frontend/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 00000000..f1182404 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,16 @@ + + +
+ +
diff --git a/frontend/src/lib/components/ui/dialog/dialog-header.svelte b/frontend/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 00000000..f3272695 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,13 @@ + + +
+ +
diff --git a/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte b/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 00000000..32a0a40b --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,21 @@ + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-portal.svelte b/frontend/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 00000000..eb5d0a57 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dialog/dialog-title.svelte b/frontend/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 00000000..e9444961 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dialog/index.ts b/frontend/src/lib/components/ui/dialog/index.ts new file mode 100644 index 00000000..b17ba5e6 --- /dev/null +++ b/frontend/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,37 @@ +import { Dialog as DialogPrimitive } from "bits-ui"; + +import Title from "./dialog-title.svelte"; +import Portal from "./dialog-portal.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; + +const Root = DialogPrimitive.Root; +const Trigger = DialogPrimitive.Trigger; +const Close = DialogPrimitive.Close; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 00000000..c825f092 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 00000000..494c6ac9 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 00000000..c97e0bb5 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 00000000..3556cca1 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 00000000..1c74ae1a --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 00000000..f7ffd4e5 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 00000000..825db690 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,14 @@ + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 00000000..b1b59f81 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 00000000..fa08da1a --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,30 @@ + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 00000000..ba10d23e --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,32 @@ + + + + + + diff --git a/frontend/src/lib/components/ui/dropdown-menu/index.ts b/frontend/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 00000000..c1749e92 --- /dev/null +++ b/frontend/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,48 @@ +import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; + +const Sub = DropdownMenuPrimitive.Sub; +const Root = DropdownMenuPrimitive.Root; +const Trigger = DropdownMenuPrimitive.Trigger; +const Group = DropdownMenuPrimitive.Group; + +export { + Sub, + Root, + Item, + Label, + Group, + Trigger, + Content, + Shortcut, + Separator, + RadioItem, + SubContent, + SubTrigger, + RadioGroup, + CheckboxItem, + // + Root as DropdownMenu, + Sub as DropdownMenuSub, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + Group as DropdownMenuGroup, + Content as DropdownMenuContent, + Trigger as DropdownMenuTrigger, + Shortcut as DropdownMenuShortcut, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + RadioGroup as DropdownMenuRadioGroup, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + CheckboxItem as DropdownMenuCheckboxItem, +}; diff --git a/frontend/src/lib/components/ui/form/form-button.svelte b/frontend/src/lib/components/ui/form/form-button.svelte new file mode 100644 index 00000000..087c839e --- /dev/null +++ b/frontend/src/lib/components/ui/form/form-button.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/frontend/src/lib/components/ui/form/form-description.svelte b/frontend/src/lib/components/ui/form/form-description.svelte new file mode 100644 index 00000000..d5680c60 --- /dev/null +++ b/frontend/src/lib/components/ui/form/form-description.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/frontend/src/lib/components/ui/form/form-element-field.svelte b/frontend/src/lib/components/ui/form/form-element-field.svelte new file mode 100644 index 00000000..5be930d3 --- /dev/null +++ b/frontend/src/lib/components/ui/form/form-element-field.svelte @@ -0,0 +1,25 @@ + + + + + +
+ +
+
diff --git a/frontend/src/lib/components/ui/form/form-field-errors.svelte b/frontend/src/lib/components/ui/form/form-field-errors.svelte new file mode 100644 index 00000000..dde1851b --- /dev/null +++ b/frontend/src/lib/components/ui/form/form-field-errors.svelte @@ -0,0 +1,26 @@ + + + + + {#each errors as error} +
{error}
+ {/each} +
+
diff --git a/frontend/src/lib/components/ui/form/form-field.svelte b/frontend/src/lib/components/ui/form/form-field.svelte new file mode 100644 index 00000000..4cf05445 --- /dev/null +++ b/frontend/src/lib/components/ui/form/form-field.svelte @@ -0,0 +1,25 @@ + + + + + +
+ +
+
diff --git a/frontend/src/lib/components/ui/form/form-fieldset.svelte b/frontend/src/lib/components/ui/form/form-fieldset.svelte new file mode 100644 index 00000000..ea58c81f --- /dev/null +++ b/frontend/src/lib/components/ui/form/form-fieldset.svelte @@ -0,0 +1,30 @@ + + + + + + + diff --git a/frontend/src/lib/components/ui/form/form-label.svelte b/frontend/src/lib/components/ui/form/form-label.svelte new file mode 100644 index 00000000..619cfb56 --- /dev/null +++ b/frontend/src/lib/components/ui/form/form-label.svelte @@ -0,0 +1,17 @@ + + + diff --git a/frontend/src/lib/components/ui/form/form-legend.svelte b/frontend/src/lib/components/ui/form/form-legend.svelte new file mode 100644 index 00000000..c19ea340 --- /dev/null +++ b/frontend/src/lib/components/ui/form/form-legend.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/frontend/src/lib/components/ui/form/index.ts b/frontend/src/lib/components/ui/form/index.ts new file mode 100644 index 00000000..0713927c --- /dev/null +++ b/frontend/src/lib/components/ui/form/index.ts @@ -0,0 +1,33 @@ +import * as FormPrimitive from "formsnap"; +import Description from "./form-description.svelte"; +import Label from "./form-label.svelte"; +import FieldErrors from "./form-field-errors.svelte"; +import Field from "./form-field.svelte"; +import Fieldset from "./form-fieldset.svelte"; +import Legend from "./form-legend.svelte"; +import ElementField from "./form-element-field.svelte"; +import Button from "./form-button.svelte"; + +const Control = FormPrimitive.Control; + +export { + Field, + Control, + Label, + Button, + FieldErrors, + Description, + Fieldset, + Legend, + ElementField, + // + Field as FormField, + Control as FormControl, + Description as FormDescription, + Label as FormLabel, + FieldErrors as FormFieldErrors, + Fieldset as FormFieldset, + Legend as FormLegend, + ElementField as FormElementField, + Button as FormButton, +}; diff --git a/frontend/src/lib/components/ui/input/index.ts b/frontend/src/lib/components/ui/input/index.ts new file mode 100644 index 00000000..75e3bc25 --- /dev/null +++ b/frontend/src/lib/components/ui/input/index.ts @@ -0,0 +1,29 @@ +import Root from "./input.svelte"; + +export type FormInputEvent = T & { + currentTarget: EventTarget & HTMLInputElement; +}; +export type InputEvents = { + blur: FormInputEvent; + change: FormInputEvent; + click: FormInputEvent; + focus: FormInputEvent; + focusin: FormInputEvent; + focusout: FormInputEvent; + keydown: FormInputEvent; + keypress: FormInputEvent; + keyup: FormInputEvent; + mouseover: FormInputEvent; + mouseenter: FormInputEvent; + mouseleave: FormInputEvent; + mousemove: FormInputEvent; + paste: FormInputEvent; + input: FormInputEvent; + wheel: FormInputEvent; +}; + +export { + Root, + // + Root as Input, +}; diff --git a/frontend/src/lib/components/ui/input/input.svelte b/frontend/src/lib/components/ui/input/input.svelte new file mode 100644 index 00000000..1fda5277 --- /dev/null +++ b/frontend/src/lib/components/ui/input/input.svelte @@ -0,0 +1,42 @@ + + + diff --git a/frontend/src/lib/components/ui/label/index.ts b/frontend/src/lib/components/ui/label/index.ts new file mode 100644 index 00000000..8bfca0b3 --- /dev/null +++ b/frontend/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label, +}; diff --git a/frontend/src/lib/components/ui/label/label.svelte b/frontend/src/lib/components/ui/label/label.svelte new file mode 100644 index 00000000..ba2e520e --- /dev/null +++ b/frontend/src/lib/components/ui/label/label.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/frontend/src/lib/components/ui/pagination/index.ts b/frontend/src/lib/components/ui/pagination/index.ts new file mode 100644 index 00000000..d83c7a94 --- /dev/null +++ b/frontend/src/lib/components/ui/pagination/index.ts @@ -0,0 +1,25 @@ +import Root from "./pagination.svelte"; +import Content from "./pagination-content.svelte"; +import Item from "./pagination-item.svelte"; +import Link from "./pagination-link.svelte"; +import PrevButton from "./pagination-prev-button.svelte"; +import NextButton from "./pagination-next-button.svelte"; +import Ellipsis from "./pagination-ellipsis.svelte"; + +export { + Root, + Content, + Item, + Link, + PrevButton, + NextButton, + Ellipsis, + // + Root as Pagination, + Content as PaginationContent, + Item as PaginationItem, + Link as PaginationLink, + PrevButton as PaginationPrevButton, + NextButton as PaginationNextButton, + Ellipsis as PaginationEllipsis, +}; diff --git a/frontend/src/lib/components/ui/pagination/pagination-content.svelte b/frontend/src/lib/components/ui/pagination/pagination-content.svelte new file mode 100644 index 00000000..2c1b9b32 --- /dev/null +++ b/frontend/src/lib/components/ui/pagination/pagination-content.svelte @@ -0,0 +1,13 @@ + + +
    + +
diff --git a/frontend/src/lib/components/ui/pagination/pagination-ellipsis.svelte b/frontend/src/lib/components/ui/pagination/pagination-ellipsis.svelte new file mode 100644 index 00000000..20991fbd --- /dev/null +++ b/frontend/src/lib/components/ui/pagination/pagination-ellipsis.svelte @@ -0,0 +1,19 @@ + + + + + More pages + diff --git a/frontend/src/lib/components/ui/pagination/pagination-item.svelte b/frontend/src/lib/components/ui/pagination/pagination-item.svelte new file mode 100644 index 00000000..0c66ea2c --- /dev/null +++ b/frontend/src/lib/components/ui/pagination/pagination-item.svelte @@ -0,0 +1,13 @@ + + +
  • + +
  • diff --git a/frontend/src/lib/components/ui/pagination/pagination-link.svelte b/frontend/src/lib/components/ui/pagination/pagination-link.svelte new file mode 100644 index 00000000..1a059f05 --- /dev/null +++ b/frontend/src/lib/components/ui/pagination/pagination-link.svelte @@ -0,0 +1,34 @@ + + + + {page.value} + diff --git a/frontend/src/lib/components/ui/pagination/pagination-next-button.svelte b/frontend/src/lib/components/ui/pagination/pagination-next-button.svelte new file mode 100644 index 00000000..f55566f2 --- /dev/null +++ b/frontend/src/lib/components/ui/pagination/pagination-next-button.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/lib/components/ui/pagination/pagination-prev-button.svelte b/frontend/src/lib/components/ui/pagination/pagination-prev-button.svelte new file mode 100644 index 00000000..9409ce06 --- /dev/null +++ b/frontend/src/lib/components/ui/pagination/pagination-prev-button.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/lib/components/ui/pagination/pagination.svelte b/frontend/src/lib/components/ui/pagination/pagination.svelte new file mode 100644 index 00000000..5f0f89f6 --- /dev/null +++ b/frontend/src/lib/components/ui/pagination/pagination.svelte @@ -0,0 +1,33 @@ + + + + + diff --git a/frontend/src/lib/components/ui/separator/index.ts b/frontend/src/lib/components/ui/separator/index.ts new file mode 100644 index 00000000..82442d2c --- /dev/null +++ b/frontend/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/frontend/src/lib/components/ui/separator/separator.svelte b/frontend/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 00000000..b3a298ff --- /dev/null +++ b/frontend/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,22 @@ + + + diff --git a/frontend/src/lib/components/ui/sonner/index.ts b/frontend/src/lib/components/ui/sonner/index.ts new file mode 100644 index 00000000..1ad9f4a2 --- /dev/null +++ b/frontend/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./sonner.svelte"; diff --git a/frontend/src/lib/components/ui/sonner/sonner.svelte b/frontend/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 00000000..7d5b2f14 --- /dev/null +++ b/frontend/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/components/ui/table/index.ts b/frontend/src/lib/components/ui/table/index.ts new file mode 100644 index 00000000..14695c81 --- /dev/null +++ b/frontend/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/frontend/src/lib/components/ui/table/table-body.svelte b/frontend/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 00000000..a3d3943b --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/frontend/src/lib/components/ui/table/table-caption.svelte b/frontend/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 00000000..ea739563 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/frontend/src/lib/components/ui/table/table-cell.svelte b/frontend/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 00000000..bd7f4a64 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/frontend/src/lib/components/ui/table/table-footer.svelte b/frontend/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 00000000..c1daa9cb --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/frontend/src/lib/components/ui/table/table-head.svelte b/frontend/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 00000000..fc4ad16c --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/lib/components/ui/table/table-header.svelte b/frontend/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 00000000..490ce7e5 --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,14 @@ + + + + + + diff --git a/frontend/src/lib/components/ui/table/table-row.svelte b/frontend/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 00000000..01648e1c --- /dev/null +++ b/frontend/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/frontend/src/lib/components/ui/table/table.svelte b/frontend/src/lib/components/ui/table/table.svelte new file mode 100644 index 00000000..72be06fe --- /dev/null +++ b/frontend/src/lib/components/ui/table/table.svelte @@ -0,0 +1,15 @@ + + +
    + + +
    +
    diff --git a/frontend/src/lib/components/web-authn-unsupported.svelte b/frontend/src/lib/components/web-authn-unsupported.svelte new file mode 100644 index 00000000..25e9e897 --- /dev/null +++ b/frontend/src/lib/components/web-authn-unsupported.svelte @@ -0,0 +1,13 @@ + + +
    +
    + +
    +

    Browser unsupported

    +

    + This browser doesn't support passkeys. Please use a browser that supports WebAuthn to sign in. +

    +
    diff --git a/frontend/src/lib/icons/checkmark-animated.svelte b/frontend/src/lib/icons/checkmark-animated.svelte new file mode 100644 index 00000000..274e74e6 --- /dev/null +++ b/frontend/src/lib/icons/checkmark-animated.svelte @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/lib/icons/connect-arrow.svelte b/frontend/src/lib/icons/connect-arrow.svelte new file mode 100644 index 00000000..3d8f34d4 --- /dev/null +++ b/frontend/src/lib/icons/connect-arrow.svelte @@ -0,0 +1,38 @@ + + + + diff --git a/frontend/src/lib/icons/cross-animated.svelte b/frontend/src/lib/icons/cross-animated.svelte new file mode 100644 index 00000000..ab0291f4 --- /dev/null +++ b/frontend/src/lib/icons/cross-animated.svelte @@ -0,0 +1,35 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts new file mode 100644 index 00000000..856f2b6c --- /dev/null +++ b/frontend/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/frontend/src/lib/services/api-service.ts b/frontend/src/lib/services/api-service.ts new file mode 100644 index 00000000..53af274f --- /dev/null +++ b/frontend/src/lib/services/api-service.ts @@ -0,0 +1,23 @@ +import { browser } from '$app/environment'; +import { env } from '$env/dynamic/public'; +import axios from 'axios'; + +abstract class APIService { + baseURL: string = '/api'; + api = axios.create({ + withCredentials: true + }); + + constructor(accessToken?: string) { + if (accessToken) { + this.api.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; + } else { + this.api.defaults.baseURL = '/api'; + } + if (!browser) { + this.api.defaults.baseURL = (env.PUBLIC_APP_URL ?? 'http://localhost') + '/api'; + } + } +} + +export default APIService; diff --git a/frontend/src/lib/services/application-configuration-service.ts b/frontend/src/lib/services/application-configuration-service.ts new file mode 100644 index 00000000..e517fe1f --- /dev/null +++ b/frontend/src/lib/services/application-configuration-service.ts @@ -0,0 +1,49 @@ +import type { + AllApplicationConfiguration, + ApplicationConfigurationRawResponse +} from '$lib/types/application-configuration'; +import APIService from './api-service'; + +export default class ApplicationConfigurationService extends APIService { + async list(showAll = false) { + const { data } = await this.api.get( + '/application-configuration', + { + params: { showAll } + } + ); + + const applicationConfiguration: Partial = {}; + data.forEach(({ key, value }) => { + (applicationConfiguration as any)[key] = value; + }); + + return applicationConfiguration as AllApplicationConfiguration; + } + + async update(applicationConfiguration: AllApplicationConfiguration) { + const res = await this.api.put('/application-configuration', applicationConfiguration); + return res.data as AllApplicationConfiguration; + } + + async updateFavicon(favicon: File) { + const formData = new FormData(); + formData.append('file', favicon!); + + await this.api.put(`/application-configuration/favicon`, formData); + } + + async updateLogo(logo: File) { + const formData = new FormData(); + formData.append('file', logo!); + + await this.api.put(`/application-configuration/logo`, formData); + } + + async updateBackgroundImage(backgroundImage: File) { + const formData = new FormData(); + formData.append('file', backgroundImage!); + + await this.api.put(`/application-configuration/background-image`, formData); + } +} diff --git a/frontend/src/lib/services/oidc-service.ts b/frontend/src/lib/services/oidc-service.ts new file mode 100644 index 00000000..4d4ff266 --- /dev/null +++ b/frontend/src/lib/services/oidc-service.ts @@ -0,0 +1,80 @@ +import type { OidcClient, OidcClientCreate } from '$lib/types/oidc.type'; +import type { Paginated, PaginationRequest } from '$lib/types/pagination.type'; +import APIService from './api-service'; + +class OidcService extends APIService { + async authorize(clientId: string, scope: string, nonce?: string) { + const res = await this.api.post('/oidc/authorize', { + scope, + nonce, + clientId + }); + + return res.data.code as string; + } + + async authorizeNewClient(clientId: string, scope: string, nonce?: string) { + const res = await this.api.post('/oidc/authorize/new-client', { + scope, + nonce, + clientId + }); + + return res.data.code as string; + } + + async listClients(search?: string, pagination?: PaginationRequest) { + const page = pagination?.page || 1; + const limit = pagination?.limit || 10; + + const res = await this.api.get('/oidc/clients', { + params: { + search, + page, + limit + } + }); + return res.data as Paginated; + } + + async createClient(client: OidcClientCreate) { + return (await this.api.post('/oidc/clients', client)).data as OidcClient; + } + + async removeClient(id: string) { + await this.api.delete(`/oidc/clients/${id}`); + } + + async getClient(id: string) { + return (await this.api.get(`/oidc/clients/${id}`)).data as OidcClient; + } + + async updateClient(id: string, client: OidcClientCreate) { + return (await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient; + } + + async updateClientLogo(client: OidcClient, image: File | null) { + if (client.hasLogo && !image) { + await this.removeClientLogo(client.id); + return; + } + if (!client.hasLogo && !image) { + return; + } + + const formData = new FormData(); + formData.append('file', image!); + + await this.api.post(`/oidc/clients/${client.id}/logo`, formData); + } + + async removeClientLogo(id: string) { + await this.api.delete(`/oidc/clients/${id}/logo`); + } + + async createClientSecret(id: string) { + return (await this.api.post(`/oidc/clients/${id}/secret`)).data.secret as string; + } +} + +export default OidcService; diff --git a/frontend/src/lib/services/user-service.ts b/frontend/src/lib/services/user-service.ts new file mode 100644 index 00000000..3bec9be5 --- /dev/null +++ b/frontend/src/lib/services/user-service.ts @@ -0,0 +1,61 @@ +import type { Paginated, PaginationRequest } from '$lib/types/pagination.type'; +import type { User, UserCreate } from '$lib/types/user.type'; +import APIService from './api-service'; + +export default class UserService extends APIService { + async list(search?: string, pagination?: PaginationRequest) { + const page = pagination?.page || 1; + const limit = pagination?.limit || 10; + + const res = await this.api.get('/users', { + params: { + search, + page, + limit + } + }); + return res.data as Paginated; + } + + async get(id: string) { + const res = await this.api.get(`/users/${id}`); + return res.data as User; + } + + async getCurrent() { + const res = await this.api.get('/users/me'); + return res.data as User; + } + + async create(user: UserCreate) { + const res = await this.api.post('/users', user); + return res.data as User; + } + + async update(id: string, user: UserCreate) { + const res = await this.api.put(`/users/${id}`, user); + return res.data as User; + } + + async updateCurrent(user: UserCreate) { + const res = await this.api.put('/users/me', user); + return res.data as User; + } + + async remove(id: string) { + await this.api.delete(`/users/${id}`); + } + + async createOneTimeAccessToken(userId: string) { + const res = await this.api.post(`/users/${userId}/one-time-access-token`, { + userId, + expiresAt: new Date(Date.now() + 1000 * 60 * 5).toISOString() + }); + return res.data.token; + } + + async exchangeOneTimeAccessToken(token: string) { + const res = await this.api.post(`/one-time-access-token/${token}`); + return res.data as User; + } +} diff --git a/frontend/src/lib/services/webauthn-service.ts b/frontend/src/lib/services/webauthn-service.ts new file mode 100644 index 00000000..f559c526 --- /dev/null +++ b/frontend/src/lib/services/webauthn-service.ts @@ -0,0 +1,42 @@ +import type { Passkey } from '$lib/types/passkey.type'; +import type { User } from '$lib/types/user.type'; +import type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/types'; +import APIService from './api-service'; +import userStore from '$lib/stores/user-store'; + +class WebAuthnService extends APIService { + async getRegistrationOptions() { + return (await this.api.get(`/webauthn/register/start`)).data; + } + + async finishRegistration(body: RegistrationResponseJSON) { + return (await this.api.post(`/webauthn/register/finish`, body)).data as Passkey; + } + + async getLoginOptions() { + return (await this.api.get(`/webauthn/login/start`)).data; + } + + async finishLogin(body: AuthenticationResponseJSON) { + return (await this.api.post(`/webauthn/login/finish`, body)).data as User; + } + + async logout() { + await this.api.post(`/webauthn/logout`); + userStore.clearUser(); + } + + async listCredentials() { + return (await this.api.get(`/webauthn/credentials`)).data as Passkey[]; + } + + async removeCredential(id: string) { + await this.api.delete(`/webauthn/credentials/${id}`); + } + + async updateCredentialName(id: string, name: string) { + await this.api.patch(`/webauthn/credentials/${id}`, { name }); + } +} + +export default WebAuthnService; diff --git a/frontend/src/lib/stores/application-configuration-store.ts b/frontend/src/lib/stores/application-configuration-store.ts new file mode 100644 index 00000000..c5774fcd --- /dev/null +++ b/frontend/src/lib/stores/application-configuration-store.ts @@ -0,0 +1,22 @@ +import ApplicationConfigurationService from '$lib/services/application-configuration-service'; +import type { ApplicationConfiguration } from '$lib/types/application-configuration'; +import { writable } from 'svelte/store'; + +const applicationConfigurationStore = writable(); + +const applicationConfigurationService = new ApplicationConfigurationService(); + +const reload = async () => { + const applicationConfiguration = await applicationConfigurationService.list(); + applicationConfigurationStore.set(applicationConfiguration); +}; + +const set = (applicationConfiguration: ApplicationConfiguration) => { + applicationConfigurationStore.set(applicationConfiguration); +} + +export default { + subscribe: applicationConfigurationStore.subscribe, + reload, + set +}; diff --git a/frontend/src/lib/stores/client-secret-store.ts b/frontend/src/lib/stores/client-secret-store.ts new file mode 100644 index 00000000..f860c258 --- /dev/null +++ b/frontend/src/lib/stores/client-secret-store.ts @@ -0,0 +1,17 @@ +import { writable } from 'svelte/store'; + +const clientSecretStore = writable(null); + +const set = (user: string) => { + clientSecretStore.set(user); +}; + +const clear = () => { + clientSecretStore.set(null); +}; + +export default { + subscribe: clientSecretStore.subscribe, + set, + clear +}; diff --git a/frontend/src/lib/stores/user-store.ts b/frontend/src/lib/stores/user-store.ts new file mode 100644 index 00000000..19147c50 --- /dev/null +++ b/frontend/src/lib/stores/user-store.ts @@ -0,0 +1,18 @@ +import type { User } from '$lib/types/user.type'; +import { writable } from 'svelte/store'; + +const userStore = writable(null); + +const setUser = (user: User) => { + userStore.set(user); +}; + +const clearUser = () => { + userStore.set(null); +}; + +export default { + subscribe: userStore.subscribe, + setUser, + clearUser +}; diff --git a/frontend/src/lib/types/application-configuration.ts b/frontend/src/lib/types/application-configuration.ts new file mode 100644 index 00000000..3f566896 --- /dev/null +++ b/frontend/src/lib/types/application-configuration.ts @@ -0,0 +1,12 @@ + +export type AllApplicationConfiguration = { + appName: string; +}; + +export type ApplicationConfiguration = AllApplicationConfiguration; + +export type ApplicationConfigurationRawResponse = { + key: string; + type: string; + value: string; +}[]; \ No newline at end of file diff --git a/frontend/src/lib/types/oidc.type.ts b/frontend/src/lib/types/oidc.type.ts new file mode 100644 index 00000000..30b6cca0 --- /dev/null +++ b/frontend/src/lib/types/oidc.type.ts @@ -0,0 +1,13 @@ +export type OidcClient = { + id: string; + name: string; + logoURL: string; + callbackURL: string; + hasLogo: boolean; +}; + +export type OidcClientCreate = Omit; + +export type OidcClientCreateWithLogo = OidcClientCreate & { + logo: File | null; +}; diff --git a/frontend/src/lib/types/pagination.type.ts b/frontend/src/lib/types/pagination.type.ts new file mode 100644 index 00000000..7d87cb98 --- /dev/null +++ b/frontend/src/lib/types/pagination.type.ts @@ -0,0 +1,15 @@ +export type PaginationRequest = { + page: number; + limit: number; +}; + +export type PaginationResponse = { + totalPages: number; + totalItems: number; + currentPage: number; +}; + +export type Paginated = { + data: T[]; + pagination: PaginationResponse; +}; \ No newline at end of file diff --git a/frontend/src/lib/types/passkey.type.ts b/frontend/src/lib/types/passkey.type.ts new file mode 100644 index 00000000..9c47e935 --- /dev/null +++ b/frontend/src/lib/types/passkey.type.ts @@ -0,0 +1,5 @@ +export type Passkey = { + id: string; + name: string; + createdAt: string; +}; diff --git a/frontend/src/lib/types/user.type.ts b/frontend/src/lib/types/user.type.ts new file mode 100644 index 00000000..f22368e4 --- /dev/null +++ b/frontend/src/lib/types/user.type.ts @@ -0,0 +1,10 @@ +export type User = { + id: string; + username: string; + email: string; + firstName: string; + lastName: string; + isAdmin: boolean; +}; + +export type UserCreate = Omit; diff --git a/frontend/src/lib/utils/debounce-util.ts b/frontend/src/lib/utils/debounce-util.ts new file mode 100644 index 00000000..5c55cfa8 --- /dev/null +++ b/frontend/src/lib/utils/debounce-util.ts @@ -0,0 +1,13 @@ +export function debounced void>(func: T, delay: number) { + let debounceTimeout: number | undefined; + + return (...args: Parameters) => { + if (debounceTimeout !== undefined) { + clearTimeout(debounceTimeout); + } + + debounceTimeout = setTimeout(() => { + func(...args); + }, delay); + }; +} \ No newline at end of file diff --git a/frontend/src/lib/utils/error-util.ts b/frontend/src/lib/utils/error-util.ts new file mode 100644 index 00000000..18423f82 --- /dev/null +++ b/frontend/src/lib/utils/error-util.ts @@ -0,0 +1,36 @@ +import { WebAuthnError } from '@simplewebauthn/browser'; +import { AxiosError } from 'axios'; +import { toast } from 'svelte-sonner'; + +export function axiosErrorToast(e: unknown, message: string = 'An unknown error occurred') { + if (e instanceof AxiosError) { + message = e.response?.data.error || message; + } + toast.error(message); +} + +export function getWebauthnErrorMessage(e: unknown) { + const errors = { + ERROR_CEREMONY_ABORTED: 'The authentication process was aborted', + ERROR_AUTHENTICATOR_GENERAL_ERROR: 'An error occurred with the authenticator', + ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT: + 'The authenticator does not support discoverable credentials', + ERROR_AUTHENTICATOR_MISSING_RESIDENT_KEY_SUPPORT: + 'The authenticator does not support resident keys', + ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED: 'This passkey was previously registered', + ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG: + 'The authenticator does not support any of the requested algorithms' + }; + + let message = 'An unknown error occurred'; + if (e instanceof WebAuthnError && e.code in errors) { + message = errors[e.code as keyof typeof errors]; + } else if (e instanceof WebAuthnError && e?.message.includes('timed out')) { + message = 'The authenticator timed out'; + } else if (e instanceof AxiosError && e.response?.data.error) { + message = e.response?.data.error; + } else { + console.error(e); + } + return message; +} diff --git a/frontend/src/lib/utils/form-util.ts b/frontend/src/lib/utils/form-util.ts new file mode 100644 index 00000000..e6525ca5 --- /dev/null +++ b/frontend/src/lib/utils/form-util.ts @@ -0,0 +1,90 @@ +import { writable } from 'svelte/store'; +import { z } from 'zod'; + +export type FormInput = { + value: T; + error: string | null; +}; + +type FormInputs = { + [K in keyof T]: FormInput; +}; + +export function createForm>(schema: T, initialValues: z.infer) { + // Create a writable store for the inputs + const inputsStore = writable>>(initializeInputs(initialValues)); + + function initializeInputs(initialValues: z.infer): FormInputs> { + const inputs: FormInputs> = {} as FormInputs>; + for (const key in initialValues) { + if (Object.prototype.hasOwnProperty.call(initialValues, key)) { + inputs[key as keyof z.infer] = { + value: initialValues[key as keyof z.infer], + error: null + }; + } + } + return inputs; + } + + function validate() { + let success = true; + inputsStore.update((inputs) => { + // Extract values from inputs to validate against the schema + const values = Object.fromEntries( + Object.entries(inputs).map(([key, input]) => [key, input.value]) + ); + + const result = schema.safeParse(values); + + if (!result.success) { + success = false; + for (const input of Object.keys(inputs)) { + const error = result.error.errors.find((e) => e.path[0] === input); + if (error) { + inputs[input as keyof z.infer].error = error.message; + } else { + inputs[input as keyof z.infer].error = null; + } + } + } else { + for (const input of Object.keys(inputs)) { + inputs[input as keyof z.infer].error = null; + } + } + return inputs; + }); + return success ? data() : null; + } + + function data() { + let values: z.infer | null = null; + inputsStore.subscribe((inputs) => { + values = Object.fromEntries( + Object.entries(inputs).map(([key, input]) => [key, input.value]) + ) as z.infer; + })(); + + return values; + } + + function reset() { + inputsStore.update((inputs) => { + for (const input of Object.keys(inputs)) { + inputs[input as keyof z.infer] = { + value: initialValues[input as keyof z.infer], + error: null + }; + } + return inputs; + }); + } + + return { + schema, + inputs: inputsStore, + data, + validate, + reset + }; +} diff --git a/frontend/src/lib/utils/style.ts b/frontend/src/lib/utils/style.ts new file mode 100644 index 00000000..88712453 --- /dev/null +++ b/frontend/src/lib/utils/style.ts @@ -0,0 +1,62 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import { cubicOut } from "svelte/easing"; +import type { TransitionConfig } from "svelte/transition"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +type FlyAndScaleParams = { + y?: number; + x?: number; + start?: number; + duration?: number; +}; + +export const flyAndScale = ( + node: Element, + params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 } +): TransitionConfig => { + const style = getComputedStyle(node); + const transform = style.transform === "none" ? "" : style.transform; + + const scaleConversion = ( + valueA: number, + scaleA: [number, number], + scaleB: [number, number] + ) => { + const [minA, maxA] = scaleA; + const [minB, maxB] = scaleB; + + const percentage = (valueA - minA) / (maxA - minA); + const valueB = percentage * (maxB - minB) + minB; + + return valueB; + }; + + const styleToString = ( + style: Record + ): string => { + return Object.keys(style).reduce((str, key) => { + if (style[key] === undefined) return str; + return str + `${key}:${style[key]};`; + }, ""); + }; + + return { + duration: params.duration ?? 200, + delay: 0, + css: (t) => { + const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]); + const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]); + const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]); + + return styleToString({ + transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`, + opacity: t + }); + }, + easing: cubicOut + }; +}; \ No newline at end of file diff --git a/frontend/src/routes/+error.svelte b/frontend/src/routes/+error.svelte new file mode 100644 index 00000000..fe39199f --- /dev/null +++ b/frontend/src/routes/+error.svelte @@ -0,0 +1,6 @@ + + + diff --git a/frontend/src/routes/+layout.server.ts b/frontend/src/routes/+layout.server.ts new file mode 100644 index 00000000..3336ade2 --- /dev/null +++ b/frontend/src/routes/+layout.server.ts @@ -0,0 +1,29 @@ +import ApplicationConfigurationService from '$lib/services/application-configuration-service'; +import UserService from '$lib/services/user-service'; +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ cookies }) => { + const userService = new UserService(cookies.get('access_token')); + const applicationConfigurationService = new ApplicationConfigurationService( + cookies.get('access_token') + ); + + const user = await userService + .getCurrent() + .then((user) => user) + .catch(() => null); + + const applicationConfiguration = await applicationConfigurationService + .list() + .then((config) => config) + .catch((e) => { + console.error( + `Failed to get application configuration: ${e.response?.data.error || e.message}` + ); + return null; + }); + return { + user, + applicationConfiguration + }; +}; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte new file mode 100644 index 00000000..dc386a6d --- /dev/null +++ b/frontend/src/routes/+layout.svelte @@ -0,0 +1,43 @@ + + +{#if !applicationConfiguration} + +{:else} +
    + {@render children()} +{/if} + + + diff --git a/frontend/src/routes/+page.server.ts b/frontend/src/routes/+page.server.ts new file mode 100644 index 00000000..4ffa51f0 --- /dev/null +++ b/frontend/src/routes/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + return redirect(302, '/login'); +}; diff --git a/frontend/src/routes/authorize/+page.server.ts b/frontend/src/routes/authorize/+page.server.ts new file mode 100644 index 00000000..57835280 --- /dev/null +++ b/frontend/src/routes/authorize/+page.server.ts @@ -0,0 +1,16 @@ +import OidcService from '$lib/services/oidc-service'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ url, cookies }) => { + const clientId = url.searchParams.get('client_id'); + const oidcService = new OidcService(cookies.get('access_token')); + + const client = await oidcService.getClient(clientId!); + + return { + scope: url.searchParams.get('scope')!, + nonce: url.searchParams.get('nonce') || undefined, + state: url.searchParams.get('state')!, + client + }; +}; diff --git a/frontend/src/routes/authorize/+page.svelte b/frontend/src/routes/authorize/+page.svelte new file mode 100644 index 00000000..76f7d505 --- /dev/null +++ b/frontend/src/routes/authorize/+page.svelte @@ -0,0 +1,131 @@ + + + + Sign in to {client.name} + + +{#if client == null} +

    Client not found

    +{:else} + + +

    Sign in to {client.name}

    + {#if !authorizationRequired} +

    + {#if errorMessage} + {errorMessage}. Please try again. + {:else} + Do you want to sign in to {client.name} with your + {$applicationConfigurationStore.appName} account? + {/if} +

    + {:else} +
    + + +

    + {client.name} wants to access the following information: +

    +
    + +
    + {#if scope!.includes('email')} + + {/if} + {#if scope!.includes('profile')} + + {/if} +
    +
    +
    +
    + {/if} +
    + + {#if !errorMessage} + + {:else} + + {/if} +
    +
    +{/if} diff --git a/frontend/src/routes/authorize/components/client-provider-images.svelte b/frontend/src/routes/authorize/components/client-provider-images.svelte new file mode 100644 index 00000000..a4335d7d --- /dev/null +++ b/frontend/src/routes/authorize/components/client-provider-images.svelte @@ -0,0 +1,70 @@ + + +
    +
    + +
    + + +
    + {#if animationDone && success} +
    + +
    + {:else if animationDone && error} +
    + +
    + {:else if client.hasLogo} + Client Logo + {:else} +
    + {client.name.charAt(0).toUpperCase()} +
    + {/if} +
    +
    diff --git a/frontend/src/routes/authorize/components/scope-item.svelte b/frontend/src/routes/authorize/components/scope-item.svelte new file mode 100644 index 00000000..0c3827ed --- /dev/null +++ b/frontend/src/routes/authorize/components/scope-item.svelte @@ -0,0 +1,13 @@ + + +
    +
    +
    +

    {name}

    +

    {description}

    +
    +
    diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte new file mode 100644 index 00000000..49985369 --- /dev/null +++ b/frontend/src/routes/login/+page.svelte @@ -0,0 +1,49 @@ + + + + Sign In + + + +
    +
    + +
    +
    +

    + Sign in to {$applicationConfigurationStore.appName} +

    +

    + Authenticate yourself with your passkey to access the admin panel +

    + +
    diff --git a/frontend/src/routes/login/[token]/+page.server.ts b/frontend/src/routes/login/[token]/+page.server.ts new file mode 100644 index 00000000..66461db2 --- /dev/null +++ b/frontend/src/routes/login/[token]/+page.server.ts @@ -0,0 +1,7 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params }) => { + return { + token: params.token + }; +}; diff --git a/frontend/src/routes/login/[token]/+page.svelte b/frontend/src/routes/login/[token]/+page.svelte new file mode 100644 index 00000000..762ab74a --- /dev/null +++ b/frontend/src/routes/login/[token]/+page.svelte @@ -0,0 +1,43 @@ + + + +
    +
    + +
    +
    +

    One Time Access

    +

    + You've been granted one-time access to your {$applicationConfigurationStore.appName} account. Please note that if you continue, + this link will become invalid. To avoid this, make sure to add a passkey. Otherwise, you'll need + to request a new link. +

    + +
    diff --git a/frontend/src/routes/settings/+layout.svelte b/frontend/src/routes/settings/+layout.svelte new file mode 100644 index 00000000..ef70caf0 --- /dev/null +++ b/frontend/src/routes/settings/+layout.svelte @@ -0,0 +1,46 @@ + + +
    +
    +
    +
    +

    Settings

    +
    +
    + +
    + {@render children()} +
    +
    +
    +
    +
    diff --git a/frontend/src/routes/settings/+page.server.ts b/frontend/src/routes/settings/+page.server.ts new file mode 100644 index 00000000..02a6030e --- /dev/null +++ b/frontend/src/routes/settings/+page.server.ts @@ -0,0 +1,5 @@ +import { redirect } from '@sveltejs/kit'; + +export function load() { + throw redirect(307, '/settings/account'); +} \ No newline at end of file diff --git a/frontend/src/routes/settings/account/+page.server.ts b/frontend/src/routes/settings/account/+page.server.ts new file mode 100644 index 00000000..18064e8f --- /dev/null +++ b/frontend/src/routes/settings/account/+page.server.ts @@ -0,0 +1,14 @@ +import UserService from '$lib/services/user-service'; +import WebAuthnService from '$lib/services/webauthn-service'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies }) => { + const webauthnService = new WebAuthnService(cookies.get('access_token')); + const userService = new UserService(cookies.get('access_token')); + const account = await userService.getCurrent(); + const passkeys = await webauthnService.listCredentials(); + return { + account, + passkeys + }; +}; diff --git a/frontend/src/routes/settings/account/+page.svelte b/frontend/src/routes/settings/account/+page.svelte new file mode 100644 index 00000000..3eeaa1e7 --- /dev/null +++ b/frontend/src/routes/settings/account/+page.svelte @@ -0,0 +1,84 @@ + + + + Account Settings + + + + + Account Details + + + + + + + + +
    +
    + Passkeys + + Manage your passkeys that you can use to authenticate yourself. + +
    + +
    +
    + {#if passkeys.length != 0} + + + + {/if} +
    + (passkeys = await webauthnService.listCredentials())} +/> diff --git a/frontend/src/routes/settings/account/account-form.svelte b/frontend/src/routes/settings/account/account-form.svelte new file mode 100644 index 00000000..d34b6a12 --- /dev/null +++ b/frontend/src/routes/settings/account/account-form.svelte @@ -0,0 +1,58 @@ + + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    diff --git a/frontend/src/routes/settings/account/passkey-list.svelte b/frontend/src/routes/settings/account/passkey-list.svelte new file mode 100644 index 00000000..4bb08c87 --- /dev/null +++ b/frontend/src/routes/settings/account/passkey-list.svelte @@ -0,0 +1,79 @@ + + +
    + {#each passkeys as passkey, i} +
    +
    + +
    +

    {passkey.name}

    +

    + Added on {new Date(passkey.createdAt).toLocaleDateString()} +

    +
    +
    +
    + + +
    +
    + {#if i !== passkeys.length - 1} + + {/if} + {/each} +
    + (passkeys = await webauthnService.listCredentials())} +/> diff --git a/frontend/src/routes/settings/account/rename-passkey-modal.svelte b/frontend/src/routes/settings/account/rename-passkey-modal.svelte new file mode 100644 index 00000000..4854be22 --- /dev/null +++ b/frontend/src/routes/settings/account/rename-passkey-modal.svelte @@ -0,0 +1,61 @@ + + + + + + Name Passkey + Name your passkey to easily identify it later. + +
    +
    + + +
    + + + +
    +
    +
    diff --git a/frontend/src/routes/settings/admin/application-configuration/+page.server.ts b/frontend/src/routes/settings/admin/application-configuration/+page.server.ts new file mode 100644 index 00000000..ab3c0014 --- /dev/null +++ b/frontend/src/routes/settings/admin/application-configuration/+page.server.ts @@ -0,0 +1,10 @@ +import ApplicationConfigurationService from '$lib/services/application-configuration-service'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies }) => { + const applicationConfigurationService = new ApplicationConfigurationService( + cookies.get('access_token') + ); + const applicationConfiguration = await applicationConfigurationService.list(); + return { applicationConfiguration }; +}; diff --git a/frontend/src/routes/settings/admin/application-configuration/+page.svelte b/frontend/src/routes/settings/admin/application-configuration/+page.svelte new file mode 100644 index 00000000..d04c9e50 --- /dev/null +++ b/frontend/src/routes/settings/admin/application-configuration/+page.svelte @@ -0,0 +1,63 @@ + + + + Application Configuration + + + + + General + + + + + + + + + Images + + + + + diff --git a/frontend/src/routes/settings/admin/application-configuration/application-configuration-form.svelte b/frontend/src/routes/settings/admin/application-configuration/application-configuration-form.svelte new file mode 100644 index 00000000..cb64ae7d --- /dev/null +++ b/frontend/src/routes/settings/admin/application-configuration/application-configuration-form.svelte @@ -0,0 +1,46 @@ + + +
    +
    +
    + +
    +
    +
    + +
    +
    diff --git a/frontend/src/routes/settings/admin/application-configuration/application-image.svelte b/frontend/src/routes/settings/admin/application-configuration/application-image.svelte new file mode 100644 index 00000000..538cc426 --- /dev/null +++ b/frontend/src/routes/settings/admin/application-configuration/application-image.svelte @@ -0,0 +1,59 @@ + + +
    + + +
    + {label} + + Update + +
    +
    +
    diff --git a/frontend/src/routes/settings/admin/application-configuration/update-application-images.svelte b/frontend/src/routes/settings/admin/application-configuration/update-application-images.svelte new file mode 100644 index 00000000..e5e144de --- /dev/null +++ b/frontend/src/routes/settings/admin/application-configuration/update-application-images.svelte @@ -0,0 +1,43 @@ + + +
    + +
    +
    + +
    diff --git a/frontend/src/routes/settings/admin/oidc-clients/+page.server.ts b/frontend/src/routes/settings/admin/oidc-clients/+page.server.ts new file mode 100644 index 00000000..3b253ca2 --- /dev/null +++ b/frontend/src/routes/settings/admin/oidc-clients/+page.server.ts @@ -0,0 +1,8 @@ +import OIDCService from '$lib/services/oidc-service'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies }) => { + const oidcService = new OIDCService(cookies.get('access_token')); + const clients = await oidcService.listClients(); + return clients; +}; diff --git a/frontend/src/routes/settings/admin/oidc-clients/+page.svelte b/frontend/src/routes/settings/admin/oidc-clients/+page.svelte new file mode 100644 index 00000000..b30a8745 --- /dev/null +++ b/frontend/src/routes/settings/admin/oidc-clients/+page.svelte @@ -0,0 +1,76 @@ + + + + OIDC Clients + + + + +
    +
    + Create OIDC Client + Add a new OIDC client to {$applicationConfigurationStore.appName}. +
    + {#if !expandAddClient} + + {:else} + + {/if} +
    +
    + {#if expandAddClient} +
    + + + +
    + {/if} +
    + + + + Manage OIDC Clients + + + + + diff --git a/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.server.ts b/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.server.ts new file mode 100644 index 00000000..6d2e7646 --- /dev/null +++ b/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.server.ts @@ -0,0 +1,7 @@ +import OidcService from '$lib/services/oidc-service'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params, cookies }) => { + const oidcService = new OidcService(cookies.get('access_token')); + return await oidcService.getClient(params.id); +}; diff --git a/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.svelte b/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.svelte new file mode 100644 index 00000000..d29a6a18 --- /dev/null +++ b/frontend/src/routes/settings/admin/oidc-clients/[id]/+page.svelte @@ -0,0 +1,102 @@ + + + + OIDC Client {client.name} + + +
    + Back +
    + + + {client.name} + + +
    + + {client.id} +
    +
    + + {$clientSecretStore ?? '••••••••••••••••••••••••••••••••'} + {#if !$clientSecretStore} + + {/if} +
    +
    +
    + + + + + diff --git a/frontend/src/routes/settings/admin/oidc-clients/client-secret.svelte b/frontend/src/routes/settings/admin/oidc-clients/client-secret.svelte new file mode 100644 index 00000000..e639a4b0 --- /dev/null +++ b/frontend/src/routes/settings/admin/oidc-clients/client-secret.svelte @@ -0,0 +1,31 @@ + + + + + + One Time Link + Use this link to sign in once. This is needed for users who haven't added a passkey yet or + have lost it. + + + + + diff --git a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte new file mode 100644 index 00000000..65e929ea --- /dev/null +++ b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-form.svelte @@ -0,0 +1,106 @@ + + +
    +
    + + +
    + +
    + {#if logoDataURL} +
    + {`${$inputs.name.value} +
    + {/if} +
    + + {#if logoDataURL} + + {/if} +
    +
    +
    +
    +
    +
    + +
    +
    diff --git a/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte new file mode 100644 index 00000000..dae4b188 --- /dev/null +++ b/frontend/src/routes/settings/admin/oidc-clients/oidc-client-list.svelte @@ -0,0 +1,145 @@ + + + + (clients = await oidcService.listClients((e.target as HTMLInputElement).value, pagination))} +/> + + + + Logo + Name + Actions + + + + {#if clients.data.length === 0} + + No OIDC clients found + + {:else} + {#each clients.data as client} + + + {#if client.hasLogo} +
    + {client.name} logo +
    + {/if} +
    + {client.name} + + + + +
    + {/each} + {/if} +
    +
    + +{#if clients?.data?.length ?? 0 > 0} + + (clients = await oidcService.listClients(search, { + page: p, + limit: pagination.limit + }))} + bind:page={clients.pagination.currentPage} + let:pages + let:currentPage + > + + + + + {#each pages as page (page.key)} + {#if page.type === 'ellipsis'} + + + + {:else} + + + {page.value} + + + {/if} + {/each} + + + + + +{/if} + + diff --git a/frontend/src/routes/settings/admin/users/+page.server.ts b/frontend/src/routes/settings/admin/users/+page.server.ts new file mode 100644 index 00000000..6ad88b96 --- /dev/null +++ b/frontend/src/routes/settings/admin/users/+page.server.ts @@ -0,0 +1,8 @@ +import UserService from '$lib/services/user-service'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ cookies }) => { + const userService = new UserService(cookies.get('access_token')); + const users = await userService.list(); + return users; +}; diff --git a/frontend/src/routes/settings/admin/users/+page.svelte b/frontend/src/routes/settings/admin/users/+page.svelte new file mode 100644 index 00000000..91f4cd6a --- /dev/null +++ b/frontend/src/routes/settings/admin/users/+page.svelte @@ -0,0 +1,74 @@ + + + + Users + + + + +
    +
    + Create User + Add a new user to {$applicationConfigurationStore.appName}. +
    + {#if !expandAddUser} + + {:else} + + {/if} +
    +
    + {#if expandAddUser} +
    + + + +
    + {/if} +
    + + + + Manage Users + + + + + diff --git a/frontend/src/routes/settings/admin/users/[id]/+page.server.ts b/frontend/src/routes/settings/admin/users/[id]/+page.server.ts new file mode 100644 index 00000000..2333230a --- /dev/null +++ b/frontend/src/routes/settings/admin/users/[id]/+page.server.ts @@ -0,0 +1,8 @@ +import UserService from '$lib/services/user-service'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params, cookies }) => { + const userService = new UserService(cookies.get('access_token')); + const user = await userService.get(params.id); + return user; +}; diff --git a/frontend/src/routes/settings/admin/users/[id]/+page.svelte b/frontend/src/routes/settings/admin/users/[id]/+page.svelte new file mode 100644 index 00000000..fc99a661 --- /dev/null +++ b/frontend/src/routes/settings/admin/users/[id]/+page.svelte @@ -0,0 +1,46 @@ + + + + User Details {user.firstName} {user.lastName} + + +
    + Back +
    + + + {user.firstName} {user.lastName} + + + + + + diff --git a/frontend/src/routes/settings/admin/users/one-time-link-modal.svelte b/frontend/src/routes/settings/admin/users/one-time-link-modal.svelte new file mode 100644 index 00000000..75b2380c --- /dev/null +++ b/frontend/src/routes/settings/admin/users/one-time-link-modal.svelte @@ -0,0 +1,33 @@ + + + + + + One Time Link + Use this link to sign in once. This is needed for users who haven't added a passkey yet or + have lost it. + +
    + + +
    +
    +
    diff --git a/frontend/src/routes/settings/admin/users/user-form.svelte b/frontend/src/routes/settings/admin/users/user-form.svelte new file mode 100644 index 00000000..3dce07c2 --- /dev/null +++ b/frontend/src/routes/settings/admin/users/user-form.svelte @@ -0,0 +1,78 @@ + + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    + +

    Admins have full access to the admin panel.

    +
    +
    +
    + +
    +
    diff --git a/frontend/src/routes/settings/admin/users/user-list.svelte b/frontend/src/routes/settings/admin/users/user-list.svelte new file mode 100644 index 00000000..e5657e3c --- /dev/null +++ b/frontend/src/routes/settings/admin/users/user-list.svelte @@ -0,0 +1,170 @@ + + + + (users = await userService.list((e.target as HTMLInputElement).value, pagination))} +/> + + + + + + Email + Username + + + Actions + + + + + {#if users.data.length === 0} + + No users found + + {:else} + {#each users.data as user} + + + + {user.email} + {user.username} + + + + + + + + createOneTimeAccessToken(user.id)} + >One-time link + Edit + deleteUser(user)} + >Delete + + + + + {/each} + {/if} + + + +{#if users?.data?.length ?? 0 > 0} + + (users = await userService.list(search, { + page: p, + limit: pagination.limit + }))} + bind:page={users.pagination.currentPage} + let:pages + let:currentPage + > + + + + + {#each pages as page (page.key)} + {#if page.type === 'ellipsis'} + + + + {:else} + + + {page.value} + + + {/if} + {/each} + + + + + +{/if} + + diff --git a/frontend/static/favicon.png b/frontend/static/favicon.png new file mode 100644 index 00000000..825b9e65 Binary files /dev/null and b/frontend/static/favicon.png differ diff --git a/frontend/static/fonts/PlayfairDisplay-Bold.woff b/frontend/static/fonts/PlayfairDisplay-Bold.woff new file mode 100644 index 00000000..3e4f6da7 Binary files /dev/null and b/frontend/static/fonts/PlayfairDisplay-Bold.woff differ diff --git a/frontend/static/fonts/PlayfairDisplay-ExtraBold.woff b/frontend/static/fonts/PlayfairDisplay-ExtraBold.woff new file mode 100644 index 00000000..a42fd641 Binary files /dev/null and b/frontend/static/fonts/PlayfairDisplay-ExtraBold.woff differ diff --git a/frontend/static/fonts/PlayfairDisplay-Medium.woff b/frontend/static/fonts/PlayfairDisplay-Medium.woff new file mode 100644 index 00000000..8c11b8d5 Binary files /dev/null and b/frontend/static/fonts/PlayfairDisplay-Medium.woff differ diff --git a/frontend/static/fonts/PlayfairDisplay-Regular.woff b/frontend/static/fonts/PlayfairDisplay-Regular.woff new file mode 100644 index 00000000..688623bc Binary files /dev/null and b/frontend/static/fonts/PlayfairDisplay-Regular.woff differ diff --git a/frontend/static/fonts/PlayfairDisplay-SemiBold.woff b/frontend/static/fonts/PlayfairDisplay-SemiBold.woff new file mode 100644 index 00000000..76a30f32 Binary files /dev/null and b/frontend/static/fonts/PlayfairDisplay-SemiBold.woff differ diff --git a/frontend/static/images/sign-in.jpg b/frontend/static/images/sign-in.jpg new file mode 100644 index 00000000..6e59f9aa Binary files /dev/null and b/frontend/static/images/sign-in.jpg differ diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js new file mode 100644 index 00000000..468f57e3 --- /dev/null +++ b/frontend/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter(), + } +}; + +export default config; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 00000000..9363523f --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,65 @@ +import { fontFamily } from "tailwindcss/defaultTheme"; +import type { Config } from "tailwindcss"; + +const config: Config = { + darkMode: ["class"], + content: ["./src/**/*.{html,js,svelte,ts}"], + safelist: ["dark"], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px" + } + }, + extend: { + colors: { + border: "hsl(var(--border) / )", + input: "hsl(var(--input) / )", + ring: "hsl(var(--ring) / )", + background: "hsl(var(--background) / )", + foreground: "hsl(var(--foreground) / )", + primary: { + DEFAULT: "hsl(var(--primary) / )", + foreground: "hsl(var(--primary-foreground) / )" + }, + secondary: { + DEFAULT: "hsl(var(--secondary) / )", + foreground: "hsl(var(--secondary-foreground) / )" + }, + destructive: { + DEFAULT: "hsl(var(--destructive) / )", + foreground: "hsl(var(--destructive-foreground) / )" + }, + muted: { + DEFAULT: "hsl(var(--muted) / )", + foreground: "hsl(var(--muted-foreground) / )" + }, + accent: { + DEFAULT: "hsl(var(--accent) / )", + foreground: "hsl(var(--accent-foreground) / )" + }, + popover: { + DEFAULT: "hsl(var(--popover) / )", + foreground: "hsl(var(--popover-foreground) / )" + }, + card: { + DEFAULT: "hsl(var(--card) / )", + foreground: "hsl(var(--card-foreground) / )" + } + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)" + }, + fontFamily: { + sans: [...fontFamily.sans], + playfair: ["Playfair Display", ...fontFamily.serif] + } + } + }, +}; + +export default config; diff --git a/frontend/tests/account-settings.spec.ts b/frontend/tests/account-settings.spec.ts new file mode 100644 index 00000000..ebca5156 --- /dev/null +++ b/frontend/tests/account-settings.spec.ts @@ -0,0 +1,83 @@ +import test, { expect } from '@playwright/test'; +import { users } from './data'; +import { cleanupBackend } from './utils/cleanup.util'; +import passkeyUtil from './utils/passkey.util'; + +test.beforeEach(cleanupBackend); + +test('Update account details', async ({ page }) => { + await page.goto('/settings/account'); + + await page.getByLabel('Firstname').fill('Timothy'); + await page.getByLabel('Lastname').fill('Apple'); + await page.getByLabel('Email').fill('timothy.apple@test.com'); + await page.getByLabel('Username').fill('timothy'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByRole('status')).toHaveText('Account details updated successfully'); +}); + +test('Update account details fails with already taken email', async ({ page }) => { + await page.goto('/settings/account'); + + await page.getByLabel('Email').fill(users.craig.email); + + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByRole('status')).toHaveText('Email is already taken'); +}); + +test('Update account details fails with already taken username', async ({ page }) => { + await page.goto('/settings/account'); + + await page.getByLabel('Username').fill(users.craig.username); + + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByRole('status')).toHaveText('Username is already taken'); +}); + +test('Add passkey to an account', async ({ page }) => { + await page.goto('/settings/account'); + + await (await passkeyUtil.init(page)).addPasskey('new'); + + await page.click('button:text("Add Passkey")'); + + await page.getByLabel('Name', { exact: true }).fill('Test Passkey'); + await page.getByLabel('Name Passkey').getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByText('Test Passkey')).toBeVisible(); +}); + +test('Rename passkey', async ({ page }) => { + await page.goto('/settings/account'); + + await page.getByLabel('Rename').first().click(); + + await page.getByLabel('Name', { exact: true }).fill('Renamed Passkey'); + await page.getByLabel('Name Passkey').getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByText('Renamed Passkey')).toBeVisible(); +}); + +test('Delete passkey from account', async ({ page }) => { + await page.goto('/settings/account'); + + await page.getByLabel('Delete').first().click(); + await page.getByText('Delete', { exact: true }).click(); + + await expect(page.getByRole('status')).toHaveText('Passkey deleted successfully'); +}); + +test('Delete last passkey from account fails', async ({ page }) => { + await page.goto('/settings/account'); + + await page.getByLabel('Delete').first().click(); + await page.getByText('Delete', { exact: true }).click(); + + await page.getByLabel('Delete').first().click(); + await page.getByText('Delete', { exact: true }).click(); + + await expect(page.getByRole('status').first()).toHaveText('You must have at least one passkey'); +}); diff --git a/frontend/tests/application-configuration.spec.ts b/frontend/tests/application-configuration.spec.ts new file mode 100644 index 00000000..b021346b --- /dev/null +++ b/frontend/tests/application-configuration.spec.ts @@ -0,0 +1,38 @@ +import test, { expect } from '@playwright/test'; +import { cleanupBackend } from './utils/cleanup.util'; + +test.beforeEach(cleanupBackend); + +test('Update general configuration', async ({ page }) => { + await page.goto('/settings/admin/application-configuration'); + + await page.getByLabel('Name').fill('Updated Name'); + await page.getByRole('button', { name: 'Save' }).first().click(); + + await expect(page.getByTestId('application-name')).toHaveText('Updated Name'); + await expect(page.getByRole('status')).toHaveText( + 'Application configuration updated successfully' + ); +}); + +test('Update application images', async ({ page }) => { + await page.goto('/settings/admin/application-configuration'); + + await page.getByLabel('Favicon').setInputFiles('tests/assets/w3-schools-favicon.ico'); + await page.getByLabel('Logo').setInputFiles('tests/assets/pingvin-share-logo.png'); + await page.getByLabel('Background Image').setInputFiles('tests/assets/clouds.jpg'); + await page.getByRole('button', { name: 'Save' }).nth(1).click(); + + await expect(page.getByRole('status')).toHaveText('Images updated successfully'); + + await page.request + .get('/api/application-configuration/favicon') + .then((res) => expect.soft(res.status()).toBe(200)); + await page.request + .get('/api/application-configuration/logo') + .then((res) => expect.soft(res.status()).toBe(200)); + + await page.request + .get('/api/application-configuration/background-image') + .then((res) => expect.soft(res.status()).toBe(200)); +}); diff --git a/frontend/tests/assets/clouds.jpg b/frontend/tests/assets/clouds.jpg new file mode 100644 index 00000000..9a2103aa Binary files /dev/null and b/frontend/tests/assets/clouds.jpg differ diff --git a/frontend/tests/assets/nextcloud-logo.png b/frontend/tests/assets/nextcloud-logo.png new file mode 100644 index 00000000..01d8ee01 Binary files /dev/null and b/frontend/tests/assets/nextcloud-logo.png differ diff --git a/frontend/tests/assets/pingvin-share-logo.png b/frontend/tests/assets/pingvin-share-logo.png new file mode 100644 index 00000000..bb7ea122 Binary files /dev/null and b/frontend/tests/assets/pingvin-share-logo.png differ diff --git a/frontend/tests/assets/w3-schools-favicon.ico b/frontend/tests/assets/w3-schools-favicon.ico new file mode 100644 index 00000000..2f2d0eed Binary files /dev/null and b/frontend/tests/assets/w3-schools-favicon.ico differ diff --git a/frontend/tests/auth.setup.ts b/frontend/tests/auth.setup.ts new file mode 100644 index 00000000..db393b34 --- /dev/null +++ b/frontend/tests/auth.setup.ts @@ -0,0 +1,18 @@ +import { test as setup } from '@playwright/test'; +import passkeyUtil from './utils/passkey.util'; +import { cleanupBackend } from './utils/cleanup.util'; + +const authFile = 'tests/.auth/user.json'; + +setup('authenticate', async ({ page }) => { + await cleanupBackend(); + await page.goto('/login'); + + await (await passkeyUtil.init(page)).addPasskey(); + + await page.getByRole('button', { name: 'Authenticate' }).click(); + await page.waitForURL('/settings/account'); + + + await page.context().storageState({ path: authFile }); +}); diff --git a/frontend/tests/data.ts b/frontend/tests/data.ts new file mode 100644 index 00000000..2437d48d --- /dev/null +++ b/frontend/tests/data.ts @@ -0,0 +1,39 @@ +export const users = { + tim: { + id: 'f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e', + firstname: 'Tim', + lastname: 'Cook', + email: 'tim.cook@test.com', + username: 'tim' + }, + craig: { + id: '1cd19686-f9a6-43f4-a41f-14a0bf5b4036', + firstname: 'Craig', + lastname: 'Federighi', + email: 'craig.federighi@test.com', + username: 'craig' + }, + steve: { + firstname: 'Steve', + lastname: 'Jobs', + email: 'steve.jobs@test.com', + username: 'steve' + } +}; + +export const oidcClients = { + nextcloud: { + id: "3654a746-35d4-4321-ac61-0bdcff2b4055", + name: 'Nextcloud', + callbackUrl: 'http://nextcloud/auth/callback' + }, + immich: { + id: "606c7782-f2b1-49e5-8ea9-26eb1b06d018", + name: 'Immich', + callbackUrl: 'http://immich/auth/callback' + }, + pingvinShare: { + name: 'Pingvin Share', + callbackUrl: 'http://pingvin.share/auth/callback' + } +}; diff --git a/frontend/tests/oidc-client-settings.spec.ts b/frontend/tests/oidc-client-settings.spec.ts new file mode 100644 index 00000000..a08053eb --- /dev/null +++ b/frontend/tests/oidc-client-settings.spec.ts @@ -0,0 +1,66 @@ +import test, { expect } from '@playwright/test'; +import { oidcClients } from './data'; +import { cleanupBackend } from './utils/cleanup.util'; + +test.beforeEach(cleanupBackend); + +test('Create OIDC client', async ({ page }) => { + await page.goto('/settings/admin/oidc-clients'); + const oidcClient = oidcClients.pingvinShare; + + await page.getByRole('button', { name: 'Add OIDC Client' }).click(); + await page.getByLabel('Name').fill(oidcClient.name); + await page.getByLabel('Callback URL').fill(oidcClient.callbackUrl); + await page.getByLabel('logo').setInputFiles('tests/assets/pingvin-share-logo.png'); + await page.getByRole('button', { name: 'Save' }).click(); + + const clientId = await page.getByTestId('client-id').textContent(); + + await expect(page.getByRole('status')).toHaveText('OIDC client created successfully'); + expect(clientId?.length).toBe(36); + expect((await page.getByTestId('client-secret').textContent())?.length).toBe(32); + await expect(page.getByLabel('Name')).toHaveValue(oidcClient.name); + await expect(page.getByLabel('Callback URL')).toHaveValue(oidcClient.callbackUrl); + await expect(page.getByRole('img', { name: `${oidcClient.name} logo` })).toBeVisible(); + await page.request + .get(`/api/oidc/clients/${clientId}/logo`) + .then((res) => expect.soft(res.status()).toBe(200)); +}); + +test('Edit OIDC client', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`); + + await page.getByLabel('Name').fill('Nextcloud updated'); + await page.getByLabel('Callback URL').fill('http://nextcloud-updated/auth/callback'); + await page.getByLabel('logo').setInputFiles('tests/assets/nextcloud-logo.png'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByRole('status')).toHaveText('OIDC client updated successfully'); + await expect(page.getByRole('img', { name: 'Nextcloud updated logo' })).toBeVisible(); + await page.request + .get(`/api/oidc/clients/${oidcClient.id}/logo`) + .then((res) => expect.soft(res.status()).toBe(200)); +}); + +test('Create new OIDC client secret', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + await page.goto(`/settings/admin/oidc-clients/${oidcClient.id}`); + + await page.getByLabel('Create new client secret').click(); + await page.getByRole('button', { name: 'Generate' }).click(); + + await expect(page.getByRole('status')).toHaveText('New client secret created successfully'); + expect((await page.getByTestId('client-secret').textContent())?.length).toBe(32); +}); + +test('Delete OIDC client', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + await page.goto('/settings/admin/oidc-clients'); + + await page.getByRole('row', { name: oidcClient.name }).getByLabel('Delete').click(); + await page.getByText('Delete', { exact: true }).click(); + + await expect(page.getByRole('status')).toHaveText('OIDC client deleted successfully'); + await expect(page.getByRole('row', { name: oidcClient.name })).not.toBeVisible(); +}); diff --git a/frontend/tests/oidc.spec.ts b/frontend/tests/oidc.spec.ts new file mode 100644 index 00000000..ed980b3c --- /dev/null +++ b/frontend/tests/oidc.spec.ts @@ -0,0 +1,69 @@ +import test, { expect } from '@playwright/test'; +import { oidcClients } from './data'; +import { cleanupBackend } from './utils/cleanup.util'; +import passkeyUtil from './utils/passkey.util'; + +test.beforeEach(cleanupBackend); + +test('Authorize existing client', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + const urlParams = createUrlParams(oidcClient); + await page.goto(`/authorize?${urlParams.toString()}`); + + await page.getByRole('button', { name: 'Sign in' }).click(); + + // Ignore DNS resolution error as the callback URL is not reachable + await page.waitForURL(oidcClient.callbackUrl).catch((e) => { + if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED')) { + throw e; + } + }); +}); + +test('Authorize existing client while not signed in', async ({ page }) => { + const oidcClient = oidcClients.nextcloud; + const urlParams = createUrlParams(oidcClient); + await page.context().clearCookies(); + await page.goto(`/authorize?${urlParams.toString()}`); + + await (await passkeyUtil.init(page)).addPasskey(); + await page.getByRole('button', { name: 'Sign in' }).click(); + + // Ignore DNS resolution error as the callback URL is not reachable + await page.waitForURL(oidcClient.callbackUrl).catch((e) => { + if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED')) { + throw e; + } + }); +}); + +test('Authorize new client', async ({ page }) => { + const oidcClient = oidcClients.immich; + const urlParams = createUrlParams(oidcClient); + await page.goto(`/authorize?${urlParams.toString()}`); + + await page.getByRole('button', { name: 'Sign in' }).click(); + + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Email' })).toBeVisible(); + await expect(page.getByTestId('scopes').getByRole('heading', { name: 'Profile' })).toBeVisible(); + + await page.getByRole('button', { name: 'Sign in' }).click(); + + // Ignore DNS resolution error as the callback URL is not reachable + await page.waitForURL(oidcClient.callbackUrl).catch((e) => { + if (!e.message.includes('net::ERR_NAME_NOT_RESOLVED')) { + throw e; + } + }); +}); + +function createUrlParams(oidcClient: { id: string; callbackUrl: string }) { + return new URLSearchParams({ + client_id: oidcClient.id, + response_type: 'code', + scope: 'openid profile email', + redirect_uri: oidcClient.callbackUrl, + state: 'nXx-6Qr-owc1SHBa', + nonce: 'P1gN3PtpKHJgKUVcLpLjm' + }); +} diff --git a/frontend/tests/user-settings.spec.ts b/frontend/tests/user-settings.spec.ts new file mode 100644 index 00000000..167be81b --- /dev/null +++ b/frontend/tests/user-settings.spec.ts @@ -0,0 +1,120 @@ +import test, { expect } from '@playwright/test'; +import { users } from './data'; +import { cleanupBackend } from './utils/cleanup.util'; + +test.beforeEach(cleanupBackend); + +test('Create user', async ({ page }) => { + const user = users.steve; + + await page.goto('/settings/admin/users'); + + await page.getByRole('button', { name: 'Add User' }).click(); + await page.getByLabel('Firstname').fill(user.firstname); + await page.getByLabel('Lastname').fill(user.lastname); + await page.getByLabel('Email').fill(user.email); + await page.getByLabel('Username').fill(user.username); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByRole('row', { name: `${user.firstname} ${user.lastname}` })).toBeVisible(); + await expect(page.getByRole('status')).toHaveText('User created successfully'); +}); + +test('Create user fails with already taken email', async ({ page }) => { + const user = users.steve; + + await page.goto('/settings/admin/users'); + + await page.getByRole('button', { name: 'Add User' }).click(); + await page.getByLabel('Firstname').fill(user.firstname); + await page.getByLabel('Lastname').fill(user.lastname); + await page.getByLabel('Email').fill(users.tim.email); + await page.getByLabel('Username').fill(user.username); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByRole('status')).toHaveText('Email is already taken'); +}); + +test('Create one time access token', async ({ page }) => { + await page.goto('/settings/admin/users'); + + await page + .getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` }) + .getByRole('button') + .click(); + await page.getByRole('menuitem', { name: 'One-time link' }).click(); + + await expect(page.getByRole('textbox', { name: 'One Time Link' })).toHaveValue( + /http:\/\/localhost\/login\/.*/ + ); +}); + +test('Delete user', async ({ page }) => { + await page.goto('/settings/admin/users'); + + await page + .getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` }) + .getByRole('button') + .click(); + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.getByRole('status')).toHaveText('User deleted successfully'); + await expect( + page.getByRole('row', { name: `${users.craig.firstname} ${users.craig.lastname}` }) + ).not.toBeVisible(); +}); + +test('Update user', async ({ page }) => { + const user = users.craig; + + await page.goto('/settings/admin/users'); + + await page + .getByRole('row', { name: `${user.firstname} ${user.lastname}` }) + .getByRole('button') + .click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + + await page.getByLabel('Firstname').fill('Crack'); + await page.getByLabel('Lastname').fill('Apple'); + await page.getByLabel('Email').fill('crack.apple@test.com'); + await page.getByLabel('Username').fill('crack'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByRole('status')).toHaveText('User updated successfully'); +}); + +test('Update user fails with already taken email', async ({ page }) => { + const user = users.craig; + + await page.goto('/settings/admin/users'); + + await page + .getByRole('row', { name: `${user.firstname} ${user.lastname}` }) + .getByRole('button') + .click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + + await page.getByLabel('Email').fill(users.tim.email); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByRole('status')).toHaveText('Email is already taken'); +}); + +test('Update user fails with already taken username', async ({ page }) => { + const user = users.craig; + + await page.goto('/settings/admin/users'); + + await page + .getByRole('row', { name: `${user.firstname} ${user.lastname}` }) + .getByRole('button') + .click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + + await page.getByLabel('Username').fill(users.tim.username); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect(page.getByRole('status')).toHaveText('Username is already taken'); +}); diff --git a/frontend/tests/utils/cleanup.util.ts b/frontend/tests/utils/cleanup.util.ts new file mode 100644 index 00000000..fbe1eba0 --- /dev/null +++ b/frontend/tests/utils/cleanup.util.ts @@ -0,0 +1,5 @@ +import axios from 'axios'; + +export async function cleanupBackend() { + await axios.post('/api/test/reset'); +} diff --git a/frontend/tests/utils/passkey.util.ts b/frontend/tests/utils/passkey.util.ts new file mode 100644 index 00000000..ac91b20d --- /dev/null +++ b/frontend/tests/utils/passkey.util.ts @@ -0,0 +1,68 @@ +import type { CDPSession, Page } from '@playwright/test'; + +// The existing passkeys are already stored in the database +const passkeys = { + existing1: { + credentialId: 'test-credential-1', + privateKey: + 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3rNKkGApsEA1TpGiphKh6axTq3Vh6wBghLLea/YkIp+hRANCAATBw6jkpXXr0pHrtAQetxiR5cTcILG/YGDCdKrhVhNDHIu12YrF6B7Frwl3AUqEpdrYEwj3Fo3XkGgvrBIJEUmG' + }, + existing2: { + credentialId: 'test-credential-2', + privateKey: + 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg3rNKkGApsEA1TpGiphKh6axTq3Vh6wBghLLea/YkIp+hRANCAATBw6jkpXXr0pHrtAQetxiR5cTcILG/YGDCdKrhVhNDHIu12YrF6B7Frwl3AUqEpdrYEwj3Fo3XkGgvrBIJEUmG' + }, + new: { + credentialId: 'new-test-credential', + privateKey: + 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgFl2lIlRyc2G7O9D8WWrw2N8D7NTlhgWcKFY7jYxrfcmhRANCAASmvbCFrXshUvW7avTIysV9UymbhmUwGb7AonUMQPgqK2Jur7PWp9V0AIe5YMuXYH1oxsqY5CoAbdY2YsPmhYoX' + } +}; + +async function init(page: Page) { + const client = await page.context().newCDPSession(page); + await client.send('WebAuthn.enable'); + const authenticatorId = await addVirtualAuthenticator(client); + + return { + addPasskey: async (passkey?: keyof typeof passkeys) => { + await addPasskey(authenticatorId, client, passkey); + } + }; +} + +async function addVirtualAuthenticator(client: CDPSession): Promise { + const result = await client.send('WebAuthn.addVirtualAuthenticator', { + // config authenticator + options: { + protocol: 'ctap2', + transport: 'internal', + hasResidentKey: true, + hasUserVerification: true, + isUserVerified: true + } + }); + return result.authenticatorId; +} + +async function addPasskey( + authenticatorId: string, + client: CDPSession, + passkeyName?: keyof typeof passkeys +): Promise { + const passkey = passkeys[passkeyName ?? 'existing1']; + await client.send('WebAuthn.addCredential', { + authenticatorId, + credential: { + credentialId: btoa(passkey.credentialId), + isResidentCredential: true, + rpId: 'localhost', + privateKey: passkey.privateKey, + userHandle: btoa('f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e'), + signCount: Math.round((new Date().getTime() - 1704444610871) / 1000 / 2) + // signCount: 2, + } + }); +} + +export default { init }; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..fc93cbd9 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 00000000..bbf8c7da --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/scripts/create-release.sh b/scripts/create-release.sh new file mode 100644 index 00000000..b432e34d --- /dev/null +++ b/scripts/create-release.sh @@ -0,0 +1,58 @@ +# Read the current version from .version +VERSION=$(cat .version) + +# Function to increment the version +increment_version() { + local version=$1 + local part=$2 + + IFS='.' read -r -a parts <<< "$version" + if [ "$part" == "minor" ]; then + parts[1]=$((parts[1] + 1)) + parts[2]=0 + elif [ "$part" == "patch" ]; then + parts[2]=$((parts[2] + 1)) + fi + echo "${parts[0]}.${parts[1]}.${parts[2]}" +} + +RELEASE_TYPE=$1 + +if [ "$RELEASE_TYPE" == "minor" ]; then + echo "Performing minor release..." + NEW_VERSION=$(increment_version $VERSION minor) +elif [ "$RELEASE_TYPE" == "patch" ]; then + echo "Performing patch release..." + NEW_VERSION=$(increment_version $VERSION patch) +else + echo "Invalid release type. Please enter either 'minor' or 'patch'." + exit 1 +fi + +# Update the .version file with the new version +echo $NEW_VERSION > .version +git add .version + +# Check if conventional-changelog is installed, if not install it +if ! command -v conventional-changelog &> /dev/null +then + echo "conventional-changelog not found, installing..." + npm install -g conventional-changelog-cli +fi + +# Generate changelog +echo "Generating changelog..." +conventional-changelog -p conventionalcommits -i CHANGELOG.md -s +git add CHANGELOG.md + +# Commit the changes with the new version +git commit -m "release: $NEW_VERSION" + +# Create a Git tag with the new version +git tag "v$NEW_VERSION" + +# Push the commit and the tag to the repository +git push +git push --tags + +echo "Release process complete. New version: $NEW_VERSION" \ No newline at end of file diff --git a/scripts/deploy-development-image.sh b/scripts/deploy-development-image.sh new file mode 100644 index 00000000..1c8540eb --- /dev/null +++ b/scripts/deploy-development-image.sh @@ -0,0 +1 @@ +docker buildx build --push --tag stonith404/pocket-id:development --platform linux/amd64,linux/arm64 . \ No newline at end of file diff --git a/scripts/docker-entrypoint.sh b/scripts/docker-entrypoint.sh new file mode 100644 index 00000000..cff6abfe --- /dev/null +++ b/scripts/docker-entrypoint.sh @@ -0,0 +1,11 @@ + +echo "Starting frontend..." +node frontend/build & + +echo "Starting backend..." +cd backend && ./pocket-id-backend & + +echo "Starting Caddy..." +caddy start --config /etc/caddy/Caddyfile & + +wait \ No newline at end of file