From 7a4ba0537fe99f88e76ccd627cac2bbf0ede4fb4 Mon Sep 17 00:00:00 2001 From: grabowski Date: Mon, 6 Apr 2026 11:51:32 +0700 Subject: [PATCH] Initial commit: Buildfor Life Budget app Multi-company budget/project tracking tool built with SvelteKit 5, PostgreSQL (Drizzle ORM), and Tailwind CSS v4. Features: - Auth: local (email/password with Argon2) + generic OIDC - 4 roles per company: admin, manager, user, viewer - Multi-company with per-company user membership - Projects with budget allocation from company pool - Expense submission with approval workflow - Categories and tags for expense organization - Reports with spending breakdowns (by category, project, time) - CSV import for Actual Budget migration - Company audit log tracking all budget and admin actions - Remaining budget hero display on overview and budget pages - Admin-only company creation; new users wait for invitation - Deployment configs for systemd + nginx (bare metal/Proxmox) Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 13 + .gitignore | 10 + deploy/buildfor-life-budget.service | 26 + deploy/nginx.conf | 43 + deploy/setup.sh | 48 + drizzle.config.ts | 10 + package-lock.json | 4213 +++++++++++++++++ package.json | 41 + src/app.css | 1 + src/app.d.ts | 20 + src/app.html | 12 + src/hooks.server.ts | 26 + src/lib/components/layout/Sidebar.svelte | 90 + src/lib/server/audit.ts | 20 + src/lib/server/auth/index.ts | 124 + src/lib/server/auth/oidc.ts | 132 + src/lib/server/auth/password.ts | 16 + src/lib/server/authorization.ts | 59 + src/lib/server/db/index.ts | 12 + src/lib/server/db/schema.ts | 309 ++ .../server/db/schema/budget-allocations.ts | 20 + src/lib/server/db/schema/categories.ts | 16 + src/lib/server/db/schema/companies.ts | 11 + src/lib/server/db/schema/company-members.ts | 21 + src/lib/server/db/schema/expenses.ts | 46 + src/lib/server/db/schema/index.ts | 9 + src/lib/server/db/schema/projects.ts | 15 + src/lib/server/db/schema/relations.ts | 83 + src/lib/server/db/schema/sessions.ts | 10 + src/lib/server/db/schema/tags.ts | 28 + src/lib/server/db/schema/users.ts | 23 + src/lib/types/index.ts | 9 + src/lib/utils/budget.ts | 12 + src/lib/utils/currency.ts | 18 + src/lib/utils/date.ts | 16 + src/routes/(app)/+layout.server.ts | 27 + src/routes/(app)/+layout.svelte | 47 + src/routes/(app)/admin/+layout.server.ts | 7 + .../(app)/admin/settings/+page.server.ts | 8 + src/routes/(app)/admin/settings/+page.svelte | 39 + src/routes/(app)/admin/users/+page.server.ts | 38 + src/routes/(app)/admin/users/+page.svelte | 60 + src/routes/(app)/companies/+page.server.ts | 65 + src/routes/(app)/companies/+page.svelte | 138 + .../companies/[companyId]/+layout.server.ts | 37 + .../companies/[companyId]/+layout.svelte | 43 + .../companies/[companyId]/+page.server.ts | 42 + .../(app)/companies/[companyId]/+page.svelte | 135 + .../[companyId]/budget/+page.server.ts | 162 + .../companies/[companyId]/budget/+page.svelte | 263 + .../[companyId]/categories/+page.server.ts | 56 + .../[companyId]/categories/+page.svelte | 74 + .../[companyId]/expenses/+page.server.ts | 122 + .../[companyId]/expenses/+page.svelte | 112 + .../[companyId]/import/+page.server.ts | 89 + .../companies/[companyId]/import/+page.svelte | 140 + .../[companyId]/projects/+page.server.ts | 27 + .../[companyId]/projects/+page.svelte | 69 + .../projects/[projectId]/+page.server.ts | 50 + .../projects/[projectId]/+page.svelte | 92 + .../[projectId]/expenses/new/+page.server.ts | 98 + .../[projectId]/expenses/new/+page.svelte | 115 + .../[companyId]/projects/new/+page.server.ts | 38 + .../[companyId]/projects/new/+page.svelte | 56 + .../[companyId]/reports/+page.server.ts | 71 + .../[companyId]/reports/+page.svelte | 139 + .../[companyId]/settings/+page.server.ts | 149 + .../[companyId]/settings/+page.svelte | 135 + src/routes/(app)/dashboard/+page.server.ts | 61 + src/routes/(app)/dashboard/+page.svelte | 86 + src/routes/(auth)/+layout.svelte | 9 + src/routes/(auth)/login/+page.server.ts | 54 + src/routes/(auth)/login/+page.svelte | 76 + src/routes/(auth)/logout/+page.server.ts | 18 + src/routes/(auth)/oidc/+server.ts | 34 + src/routes/(auth)/oidc/callback/+server.ts | 92 + src/routes/(auth)/signup/+page.server.ts | 68 + src/routes/(auth)/signup/+page.svelte | 86 + src/routes/+layout.server.ts | 7 + src/routes/+layout.svelte | 7 + src/routes/+page.server.ts | 9 + src/routes/+page.svelte | 15 + static/favicon.png | 0 svelte.config.js | 15 + tsconfig.json | 14 + vite.config.ts | 7 + 86 files changed, 8963 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 deploy/buildfor-life-budget.service create mode 100644 deploy/nginx.conf create mode 100644 deploy/setup.sh create mode 100644 drizzle.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/hooks.server.ts create mode 100644 src/lib/components/layout/Sidebar.svelte create mode 100644 src/lib/server/audit.ts create mode 100644 src/lib/server/auth/index.ts create mode 100644 src/lib/server/auth/oidc.ts create mode 100644 src/lib/server/auth/password.ts create mode 100644 src/lib/server/authorization.ts create mode 100644 src/lib/server/db/index.ts create mode 100644 src/lib/server/db/schema.ts create mode 100644 src/lib/server/db/schema/budget-allocations.ts create mode 100644 src/lib/server/db/schema/categories.ts create mode 100644 src/lib/server/db/schema/companies.ts create mode 100644 src/lib/server/db/schema/company-members.ts create mode 100644 src/lib/server/db/schema/expenses.ts create mode 100644 src/lib/server/db/schema/index.ts create mode 100644 src/lib/server/db/schema/projects.ts create mode 100644 src/lib/server/db/schema/relations.ts create mode 100644 src/lib/server/db/schema/sessions.ts create mode 100644 src/lib/server/db/schema/tags.ts create mode 100644 src/lib/server/db/schema/users.ts create mode 100644 src/lib/types/index.ts create mode 100644 src/lib/utils/budget.ts create mode 100644 src/lib/utils/currency.ts create mode 100644 src/lib/utils/date.ts create mode 100644 src/routes/(app)/+layout.server.ts create mode 100644 src/routes/(app)/+layout.svelte create mode 100644 src/routes/(app)/admin/+layout.server.ts create mode 100644 src/routes/(app)/admin/settings/+page.server.ts create mode 100644 src/routes/(app)/admin/settings/+page.svelte create mode 100644 src/routes/(app)/admin/users/+page.server.ts create mode 100644 src/routes/(app)/admin/users/+page.svelte create mode 100644 src/routes/(app)/companies/+page.server.ts create mode 100644 src/routes/(app)/companies/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/+layout.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/+layout.svelte create mode 100644 src/routes/(app)/companies/[companyId]/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/budget/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/budget/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/categories/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/categories/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/expenses/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/expenses/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/import/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/import/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/projects/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/projects/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/projects/[projectId]/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/projects/[projectId]/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/projects/new/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/projects/new/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/reports/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/reports/+page.svelte create mode 100644 src/routes/(app)/companies/[companyId]/settings/+page.server.ts create mode 100644 src/routes/(app)/companies/[companyId]/settings/+page.svelte create mode 100644 src/routes/(app)/dashboard/+page.server.ts create mode 100644 src/routes/(app)/dashboard/+page.svelte create mode 100644 src/routes/(auth)/+layout.svelte create mode 100644 src/routes/(auth)/login/+page.server.ts create mode 100644 src/routes/(auth)/login/+page.svelte create mode 100644 src/routes/(auth)/logout/+page.server.ts create mode 100644 src/routes/(auth)/oidc/+server.ts create mode 100644 src/routes/(auth)/oidc/callback/+server.ts create mode 100644 src/routes/(auth)/signup/+page.server.ts create mode 100644 src/routes/(auth)/signup/+page.svelte create mode 100644 src/routes/+layout.server.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/+page.svelte create mode 100644 static/favicon.png create mode 100644 svelte.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..09d7e54 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Server +PORT=3000 +HOST=127.0.0.1 +ORIGIN=http://localhost:3000 + +# Database +DATABASE_URL=postgresql://budget_app:password@localhost:5432/buildfor_life_budget + +# OIDC (optional — leave blank to disable) +OIDC_ISSUER_URL= +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= +OIDC_REDIRECT_URI=http://localhost:3000/oidc/callback diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b77474 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +build/ +.svelte-kit/ +.env +.env.* +!.env.example +*.db +*.db-journal +.DS_Store +dist/ diff --git a/deploy/buildfor-life-budget.service b/deploy/buildfor-life-budget.service new file mode 100644 index 0000000..59c9466 --- /dev/null +++ b/deploy/buildfor-life-budget.service @@ -0,0 +1,26 @@ +[Unit] +Description=Buildfor Life Budget App +After=network.target postgresql.service + +[Service] +Type=simple +User=budget-app +Group=budget-app +WorkingDirectory=/opt/buildfor-life-budget +EnvironmentFile=/opt/buildfor-life-budget/.env +ExecStart=/usr/bin/node build +Restart=on-failure +RestartSec=5 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=b4l-budget + +# Security hardening +NoNewPrivileges=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/buildfor-life-budget +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..1173e73 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,43 @@ +server { + listen 80; + server_name budget.b4l.co.th; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl http2; + server_name budget.b4l.co.th; + + ssl_certificate /etc/letsencrypt/live/budget.b4l.co.th/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/budget.b4l.co.th/privkey.pem; + + # Security headers + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header Referrer-Policy strict-origin-when-cross-origin; + + # Compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml; + gzip_min_length 1000; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_cache_bypass $http_upgrade; + } + + # Cache immutable static assets + location /_app/immutable/ { + proxy_pass http://127.0.0.1:3000; + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/deploy/setup.sh b/deploy/setup.sh new file mode 100644 index 0000000..482317f --- /dev/null +++ b/deploy/setup.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Buildfor Life Budget - Server Setup Script +# Run on a fresh Debian/Ubuntu Proxmox VM + +set -euo pipefail + +APP_USER="budget-app" +APP_DIR="/opt/buildfor-life-budget" +DOMAIN="budget.b4l.co.th" + +echo "=== Installing dependencies ===" +apt-get update +apt-get install -y nginx certbot python3-certbot-nginx postgresql nodejs npm + +echo "=== Setting up PostgreSQL ===" +sudo -u postgres psql <=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@node-rs/argon2": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-2.0.2.tgz", + "integrity": "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@node-rs/argon2-android-arm-eabi": "2.0.2", + "@node-rs/argon2-android-arm64": "2.0.2", + "@node-rs/argon2-darwin-arm64": "2.0.2", + "@node-rs/argon2-darwin-x64": "2.0.2", + "@node-rs/argon2-freebsd-x64": "2.0.2", + "@node-rs/argon2-linux-arm-gnueabihf": "2.0.2", + "@node-rs/argon2-linux-arm64-gnu": "2.0.2", + "@node-rs/argon2-linux-arm64-musl": "2.0.2", + "@node-rs/argon2-linux-x64-gnu": "2.0.2", + "@node-rs/argon2-linux-x64-musl": "2.0.2", + "@node-rs/argon2-wasm32-wasi": "2.0.2", + "@node-rs/argon2-win32-arm64-msvc": "2.0.2", + "@node-rs/argon2-win32-ia32-msvc": "2.0.2", + "@node-rs/argon2-win32-x64-msvc": "2.0.2" + } + }, + "node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", + "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-android-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", + "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", + "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-freebsd-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", + "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", + "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", + "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", + "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz", + "integrity": "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-x64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz", + "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz", + "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz", + "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz", + "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz", + "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@oslojs/asn1": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@oslojs/asn1/-/asn1-1.0.0.tgz", + "integrity": "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==", + "license": "MIT", + "dependencies": { + "@oslojs/binary": "1.0.0" + } + }, + "node_modules/@oslojs/binary": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@oslojs/binary/-/binary-1.0.0.tgz", + "integrity": "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==", + "license": "MIT" + }, + "node_modules/@oslojs/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@oslojs/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==", + "license": "MIT", + "dependencies": { + "@oslojs/asn1": "1.0.0", + "@oslojs/binary": "1.0.0" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@petamoriken/float16": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", + "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", + "integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "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-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, + "license": "MIT", + "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": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "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.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "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.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.5.4.tgz", + "integrity": "sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.59.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.56.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.56.1.tgz", + "integrity": "sha512-9hDOl3yUh8UXWt+mN29dbcdrW0vNwPvMqi01y2Mw+ceErNIISh8MeEY7fXT2Dx1CjC/kfsVqrbxw7DifYr4hsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.4", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", + "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.1", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.17", + "vitefu": "^1.0.6" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": 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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", + "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "dev": true, + "license": "MIT" + }, + "node_modules/drizzle-kit": { + "version": "0.30.6", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.6.tgz", + "integrity": "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.19.7", + "esbuild-register": "^3.5.0", + "gel": "^2.0.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.38.4.tgz", + "integrity": "sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/react": ">=18", + "@types/sql.js": "*", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "react": ">=18", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/react": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", + "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@typescript-eslint/types": "^8.2.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, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gel": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/gel/-/gel-2.2.0.tgz", + "integrity": "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@petamoriken/float16": "^3.8.7", + "debug": "^4.3.4", + "env-paths": "^3.0.0", + "semver": "^7.6.2", + "shell-quote": "^1.8.1", + "which": "^4.0.0" + }, + "bin": { + "gel": "dist/cli.mjs" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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, + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.1.tgz", + "integrity": "sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.4", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz", + "integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.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 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..95c4b54 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "buildfor-life-budget", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio" + }, + "dependencies": { + "@node-rs/argon2": "^2.0.2", + "@oslojs/crypto": "^1.0.1", + "@oslojs/encoding": "^1.1.0", + "chart.js": "^4.4.7", + "date-fns": "^4.1.0", + "drizzle-orm": "^0.38.4", + "papaparse": "^5.5.2", + "pg": "^8.13.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.15.2", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/vite": "^4.1.3", + "@types/pg": "^8.11.11", + "@types/papaparse": "^5.3.15", + "drizzle-kit": "^0.30.5", + "svelte": "^5.19.0", + "svelte-check": "^4.1.4", + "tailwindcss": "^4.1.3", + "typescript": "^5.7.3", + "vite": "^6.1.0" + } +} diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..d4b5078 --- /dev/null +++ b/src/app.css @@ -0,0 +1 @@ +@import 'tailwindcss'; diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..268a1b2 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,20 @@ +declare global { + namespace App { + interface Locals { + user: { + id: string; + email: string; + username: string | null; + displayName: string | null; + isSystemAdmin: boolean; + } | null; + session: { + id: string; + expiresAt: Date; + fresh: boolean; + } | null; + } + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..77a5ff5 --- /dev/null +++ b/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..c3ac39d --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,26 @@ +import type { Handle } from '@sveltejs/kit'; +import { validateSession, setSessionCookie } from '$lib/server/auth/index.js'; + +export const handle: Handle = async ({ event, resolve }) => { + const token = event.cookies.get('session'); + + if (token) { + const { session, user } = await validateSession(token); + if (session) { + event.locals.user = user; + event.locals.session = session; + if (session.fresh) { + setSessionCookie(event, token, session.expiresAt); + } + } else { + event.locals.user = null; + event.locals.session = null; + event.cookies.delete('session', { path: '/' }); + } + } else { + event.locals.user = null; + event.locals.session = null; + } + + return resolve(event); +}; diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte new file mode 100644 index 0000000..26d7cef --- /dev/null +++ b/src/lib/components/layout/Sidebar.svelte @@ -0,0 +1,90 @@ + + + + + +{#if open} + +{/if} diff --git a/src/lib/server/audit.ts b/src/lib/server/audit.ts new file mode 100644 index 0000000..a23607c --- /dev/null +++ b/src/lib/server/audit.ts @@ -0,0 +1,20 @@ +import { db } from './db/index.js'; +import { companyLog } from './db/schema.js'; + +type LogEvent = typeof companyLog.$inferInsert['event']; + +export async function logCompanyEvent( + companyId: string, + userId: string | null, + event: LogEvent, + description: string, + metadata?: Record +): Promise { + await db.insert(companyLog).values({ + companyId, + userId, + event, + description, + metadata: metadata ? JSON.stringify(metadata) : null + }); +} diff --git a/src/lib/server/auth/index.ts b/src/lib/server/auth/index.ts new file mode 100644 index 0000000..43e0173 --- /dev/null +++ b/src/lib/server/auth/index.ts @@ -0,0 +1,124 @@ +import { + encodeBase32LowerCaseNoPadding, + encodeHexLowerCase +} from '@oslojs/encoding'; +import { sha256 } from '@oslojs/crypto/sha2'; +import { db } from '../db/index.js'; +import { sessions, users } from '../db/schema.js'; +import { eq } from 'drizzle-orm'; +import type { RequestEvent } from '@sveltejs/kit'; + +const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 days +const SESSION_REFRESH_MS = 15 * 24 * 60 * 60 * 1000; // 15 days + +export function generateSessionToken(): string { + const bytes = new Uint8Array(20); + crypto.getRandomValues(bytes); + return encodeBase32LowerCaseNoPadding(bytes); +} + +export function generateUserId(): string { + const bytes = new Uint8Array(15); + crypto.getRandomValues(bytes); + return encodeBase32LowerCaseNoPadding(bytes); +} + +function hashToken(token: string): string { + const encoded = new TextEncoder().encode(token); + return encodeHexLowerCase(sha256(encoded)); +} + +export async function createSession( + token: string, + userId: string +): Promise<{ id: string; userId: string; expiresAt: Date; fresh: boolean }> { + const sessionId = hashToken(token); + const expiresAt = new Date(Date.now() + SESSION_DURATION_MS); + + await db.insert(sessions).values({ + id: sessionId, + userId, + expiresAt + }); + + return { id: sessionId, userId, expiresAt, fresh: true }; +} + +export async function validateSession( + token: string +): Promise<{ + session: { id: string; expiresAt: Date; fresh: boolean } | null; + user: App.Locals['user']; +}> { + const sessionId = hashToken(token); + + const result = await db + .select({ + session: sessions, + user: { + id: users.id, + email: users.email, + username: users.username, + displayName: users.displayName, + isSystemAdmin: users.isSystemAdmin + } + }) + .from(sessions) + .innerJoin(users, eq(sessions.userId, users.id)) + .where(eq(sessions.id, sessionId)) + .limit(1); + + if (result.length === 0) { + return { session: null, user: null }; + } + + const { session, user } = result[0]; + + // Session expired + if (Date.now() >= session.expiresAt.getTime()) { + await db.delete(sessions).where(eq(sessions.id, sessionId)); + return { session: null, user: null }; + } + + // Extend session if within refresh window + let fresh = false; + if (Date.now() >= session.expiresAt.getTime() - SESSION_REFRESH_MS) { + const newExpiry = new Date(Date.now() + SESSION_DURATION_MS); + await db + .update(sessions) + .set({ expiresAt: newExpiry }) + .where(eq(sessions.id, sessionId)); + session.expiresAt = newExpiry; + fresh = true; + } + + return { + session: { id: session.id, expiresAt: session.expiresAt, fresh }, + user + }; +} + +export async function invalidateSession(token: string): Promise { + const sessionId = hashToken(token); + await db.delete(sessions).where(eq(sessions.id, sessionId)); +} + +export function setSessionCookie(event: RequestEvent, token: string, expiresAt: Date): void { + event.cookies.set('session', token, { + httpOnly: true, + sameSite: 'lax', + expires: expiresAt, + path: '/', + secure: event.url.protocol === 'https:' + }); +} + +export function deleteSessionCookie(event: RequestEvent): void { + event.cookies.set('session', '', { + httpOnly: true, + sameSite: 'lax', + maxAge: 0, + path: '/', + secure: event.url.protocol === 'https:' + }); +} diff --git a/src/lib/server/auth/oidc.ts b/src/lib/server/auth/oidc.ts new file mode 100644 index 0000000..e8f0b87 --- /dev/null +++ b/src/lib/server/auth/oidc.ts @@ -0,0 +1,132 @@ +import { env } from '$env/dynamic/private'; + +interface OIDCConfig { + issuerUrl: string; + clientId: string; + clientSecret: string; + redirectUri: string; + authorizationEndpoint: string; + tokenEndpoint: string; + userinfoEndpoint: string; +} + +let oidcConfig: OIDCConfig | null = null; + +export function isOIDCEnabled(): boolean { + return !!(env.OIDC_ISSUER_URL && env.OIDC_CLIENT_ID && env.OIDC_CLIENT_SECRET); +} + +export async function getOIDCConfig(): Promise { + if (oidcConfig) return oidcConfig; + + if (!isOIDCEnabled()) { + throw new Error('OIDC is not configured'); + } + + const issuerUrl = env.OIDC_ISSUER_URL!.replace(/\/$/, ''); + const discoveryUrl = `${issuerUrl}/.well-known/openid-configuration`; + const res = await fetch(discoveryUrl); + if (!res.ok) { + throw new Error(`Failed to fetch OIDC discovery: ${res.status}`); + } + + const discovery = await res.json(); + + oidcConfig = { + issuerUrl, + clientId: env.OIDC_CLIENT_ID!, + clientSecret: env.OIDC_CLIENT_SECRET!, + redirectUri: env.OIDC_REDIRECT_URI || `${env.ORIGIN}/oidc/callback`, + authorizationEndpoint: discovery.authorization_endpoint, + tokenEndpoint: discovery.token_endpoint, + userinfoEndpoint: discovery.userinfo_endpoint + }; + + return oidcConfig; +} + +export function generateState(): string { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +export function generateCodeVerifier(): string { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +export async function generateCodeChallenge(verifier: string): Promise { + const encoded = new TextEncoder().encode(verifier); + const digest = await crypto.subtle.digest('SHA-256', encoded); + return btoa(String.fromCharCode(...new Uint8Array(digest))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); +} + +export async function getAuthorizationUrl(state: string, codeVerifier: string): Promise { + const config = await getOIDCConfig(); + const codeChallenge = await generateCodeChallenge(codeVerifier); + + const params = new URLSearchParams({ + response_type: 'code', + client_id: config.clientId, + redirect_uri: config.redirectUri, + scope: 'openid email profile', + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256' + }); + + return `${config.authorizationEndpoint}?${params}`; +} + +export async function exchangeCode( + code: string, + codeVerifier: string +): Promise<{ accessToken: string; idToken?: string }> { + const config = await getOIDCConfig(); + + const res = await fetch(config.tokenEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: config.redirectUri, + client_id: config.clientId, + client_secret: config.clientSecret, + code_verifier: codeVerifier + }) + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(`Token exchange failed: ${res.status} ${text}`); + } + + const data = await res.json(); + return { accessToken: data.access_token, idToken: data.id_token }; +} + +export async function getUserInfo( + accessToken: string +): Promise<{ sub: string; email: string; name?: string }> { + const config = await getOIDCConfig(); + + const res = await fetch(config.userinfoEndpoint, { + headers: { Authorization: `Bearer ${accessToken}` } + }); + + if (!res.ok) { + throw new Error(`UserInfo request failed: ${res.status}`); + } + + return res.json(); +} diff --git a/src/lib/server/auth/password.ts b/src/lib/server/auth/password.ts new file mode 100644 index 0000000..cafc725 --- /dev/null +++ b/src/lib/server/auth/password.ts @@ -0,0 +1,16 @@ +import { hash, verify } from '@node-rs/argon2'; + +const ARGON2_OPTIONS = { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1 +}; + +export async function hashPassword(password: string): Promise { + return hash(password, ARGON2_OPTIONS); +} + +export async function verifyPassword(passwordHash: string, password: string): Promise { + return verify(passwordHash, password, ARGON2_OPTIONS); +} diff --git a/src/lib/server/authorization.ts b/src/lib/server/authorization.ts new file mode 100644 index 0000000..81c6fd9 --- /dev/null +++ b/src/lib/server/authorization.ts @@ -0,0 +1,59 @@ +import { error } from '@sveltejs/kit'; +import { db } from './db/index.js'; +import { companyMembers } from './db/schema.js'; +import { and, eq } from 'drizzle-orm'; +import { ROLE_HIERARCHY, type CompanyRole } from '$lib/types/index.js'; + +export function requireAuth(locals: App.Locals): NonNullable { + if (!locals.user) { + error(401, 'Authentication required'); + } + return locals.user; +} + +export function requireSystemAdmin(locals: App.Locals): NonNullable { + const user = requireAuth(locals); + if (!user.isSystemAdmin) { + error(403, 'System admin access required'); + } + return user; +} + +export async function getCompanyRole( + userId: string, + companyId: string +): Promise { + const result = await db + .select({ role: companyMembers.role }) + .from(companyMembers) + .where(and(eq(companyMembers.userId, userId), eq(companyMembers.companyId, companyId))) + .limit(1); + + if (result.length === 0) return null; + return result[0].role; +} + +export async function requireCompanyRole( + locals: App.Locals, + companyId: string, + minRole: CompanyRole +): Promise<{ user: NonNullable; role: CompanyRole }> { + const user = requireAuth(locals); + + // System admins bypass company role checks + if (user.isSystemAdmin) { + return { user, role: 'admin' }; + } + + const role = await getCompanyRole(user.id, companyId); + + if (!role) { + error(403, 'Not a member of this company'); + } + + if (ROLE_HIERARCHY[role] < ROLE_HIERARCHY[minRole]) { + error(403, `Requires ${minRole} role or higher`); + } + + return { user, role }; +} diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts new file mode 100644 index 0000000..ad78cdc --- /dev/null +++ b/src/lib/server/db/index.ts @@ -0,0 +1,12 @@ +import { drizzle } from 'drizzle-orm/node-postgres'; +import pg from 'pg'; +import * as schema from './schema.js'; +import { env } from '$env/dynamic/private'; + +const pool = new pg.Pool({ + connectionString: env.DATABASE_URL +}); + +export const db = drizzle(pool, { schema }); + +export type Database = typeof db; diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts new file mode 100644 index 0000000..b9e0c72 --- /dev/null +++ b/src/lib/server/db/schema.ts @@ -0,0 +1,309 @@ +import { relations, sql } from 'drizzle-orm'; +import { + pgTable, + pgEnum, + text, + boolean, + timestamp, + uniqueIndex, + uuid, + numeric, + date, + index, + primaryKey +} from 'drizzle-orm/pg-core'; + +// ── Enums ────────────────────────────────────────────── + +export const companyRoleEnum = pgEnum('company_role', ['admin', 'manager', 'user', 'viewer']); +export const expenseStatusEnum = pgEnum('expense_status', ['pending', 'approved', 'rejected']); + +// ── Users ────────────────────────────────────────────── + +export const users = pgTable( + 'users', + { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + username: text('username').unique(), + displayName: text('display_name'), + passwordHash: text('password_hash'), + oidcProvider: text('oidc_provider'), + oidcSubject: text('oidc_subject'), + isSystemAdmin: boolean('is_system_admin').notNull().default(false), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() + }, + (table) => [ + uniqueIndex('users_oidc_idx') + .on(table.oidcProvider, table.oidcSubject) + .where(sql`${table.oidcProvider} IS NOT NULL AND ${table.oidcSubject} IS NOT NULL`) + ] +); + +// ── Sessions ─────────────────────────────────────────── + +export const sessions = pgTable('sessions', { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull() +}); + +// ── Companies ────────────────────────────────────────── + +export const companies = pgTable('companies', { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + description: text('description'), + totalBudget: numeric('total_budget', { precision: 15, scale: 2 }).notNull().default('0'), + currency: text('currency').notNull().default('THB'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() +}); + +// ── Company Members ──────────────────────────────────── + +export const companyMembers = pgTable( + 'company_members', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + role: companyRoleEnum('role').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() + }, + (table) => [uniqueIndex('company_members_user_company_idx').on(table.userId, table.companyId)] +); + +// ── Projects ─────────────────────────────────────────── + +export const projects = pgTable('projects', { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + description: text('description'), + allocatedBudget: numeric('allocated_budget', { precision: 15, scale: 2 }).notNull().default('0'), + isActive: boolean('is_active').notNull().default(true), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() +}); + +// ── Categories ───────────────────────────────────────── + +export const categories = pgTable( + 'categories', + { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + color: text('color'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() + }, + (table) => [uniqueIndex('categories_company_name_idx').on(table.companyId, table.name)] +); + +// ── Expenses ─────────────────────────────────────────── + +export const expenses = pgTable( + 'expenses', + { + id: uuid('id').primaryKey().defaultRandom(), + projectId: uuid('project_id') + .notNull() + .references(() => projects.id, { onDelete: 'cascade' }), + categoryId: uuid('category_id').references(() => categories.id, { onDelete: 'set null' }), + submittedBy: text('submitted_by') + .notNull() + .references(() => users.id), + approvedBy: text('approved_by').references(() => users.id), + title: text('title').notNull(), + description: text('description'), + amount: numeric('amount', { precision: 15, scale: 2 }).notNull(), + currency: text('currency').notNull(), + receiptUrl: text('receipt_url'), + expenseDate: date('expense_date').notNull(), + status: expenseStatusEnum('status').notNull().default('pending'), + reviewedAt: timestamp('reviewed_at', { withTimezone: true }), + rejectionReason: text('rejection_reason'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() + }, + (table) => [ + index('expenses_project_status_idx').on(table.projectId, table.status), + index('expenses_submitted_by_idx').on(table.submittedBy), + index('expenses_date_idx').on(table.expenseDate) + ] +); + +// ── Tags ─────────────────────────────────────────────── + +export const tags = pgTable( + 'tags', + { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + name: text('name').notNull() + }, + (table) => [uniqueIndex('tags_company_name_idx').on(table.companyId, table.name)] +); + +export const expenseTags = pgTable( + 'expense_tags', + { + expenseId: uuid('expense_id') + .notNull() + .references(() => expenses.id, { onDelete: 'cascade' }), + tagId: uuid('tag_id') + .notNull() + .references(() => tags.id, { onDelete: 'cascade' }) + }, + (table) => [primaryKey({ columns: [table.expenseId, table.tagId] })] +); + +// ── Budget Allocations ───────────────────────────────── + +export const budgetAllocations = pgTable('budget_allocations', { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + projectId: uuid('project_id') + .notNull() + .references(() => projects.id, { onDelete: 'cascade' }), + amount: numeric('amount', { precision: 15, scale: 2 }).notNull(), + allocatedBy: text('allocated_by') + .notNull() + .references(() => users.id), + note: text('note'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() +}); + +// ── Company Log (Audit Trail) ────────────────────────── + +export const companyLogEventEnum = pgEnum('company_log_event', [ + 'company_created', + 'company_updated', + 'budget_initial', + 'budget_added', + 'budget_allocated', + 'budget_deallocated', + 'project_created', + 'project_updated', + 'member_added', + 'member_removed', + 'member_role_changed', + 'expense_submitted', + 'expense_approved', + 'expense_rejected', + 'category_created', + 'import_completed' +]); + +export const companyLog = pgTable( + 'company_log', + { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + userId: text('user_id').references(() => users.id), + event: companyLogEventEnum('event').notNull(), + description: text('description').notNull(), + metadata: text('metadata'), // JSON string for extra context (amounts, names, etc.) + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() + }, + (table) => [index('company_log_company_idx').on(table.companyId, table.createdAt)] +); + +// ── Relations ────────────────────────────────────────── + +export const usersRelations = relations(users, ({ many }) => ({ + sessions: many(sessions), + companyMemberships: many(companyMembers), + submittedExpenses: many(expenses, { relationName: 'submittedExpenses' }), + approvedExpenses: many(expenses, { relationName: 'approvedExpenses' }) +})); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }) +})); + +export const companiesRelations = relations(companies, ({ many }) => ({ + members: many(companyMembers), + projects: many(projects), + categories: many(categories), + tags: many(tags), + budgetAllocations: many(budgetAllocations), + logs: many(companyLog) +})); + +export const companyMembersRelations = relations(companyMembers, ({ one }) => ({ + user: one(users, { fields: [companyMembers.userId], references: [users.id] }), + company: one(companies, { fields: [companyMembers.companyId], references: [companies.id] }) +})); + +export const projectsRelations = relations(projects, ({ one, many }) => ({ + company: one(companies, { fields: [projects.companyId], references: [companies.id] }), + expenses: many(expenses), + budgetAllocations: many(budgetAllocations) +})); + +export const categoriesRelations = relations(categories, ({ one, many }) => ({ + company: one(companies, { fields: [categories.companyId], references: [companies.id] }), + expenses: many(expenses) +})); + +export const expensesRelations = relations(expenses, ({ one, many }) => ({ + project: one(projects, { fields: [expenses.projectId], references: [projects.id] }), + category: one(categories, { fields: [expenses.categoryId], references: [categories.id] }), + submitter: one(users, { + fields: [expenses.submittedBy], + references: [users.id], + relationName: 'submittedExpenses' + }), + approver: one(users, { + fields: [expenses.approvedBy], + references: [users.id], + relationName: 'approvedExpenses' + }), + expenseTags: many(expenseTags) +})); + +export const tagsRelations = relations(tags, ({ one, many }) => ({ + company: one(companies, { fields: [tags.companyId], references: [companies.id] }), + expenseTags: many(expenseTags) +})); + +export const expenseTagsRelations = relations(expenseTags, ({ one }) => ({ + expense: one(expenses, { fields: [expenseTags.expenseId], references: [expenses.id] }), + tag: one(tags, { fields: [expenseTags.tagId], references: [tags.id] }) +})); + +export const companyLogRelations = relations(companyLog, ({ one }) => ({ + company: one(companies, { fields: [companyLog.companyId], references: [companies.id] }), + user: one(users, { fields: [companyLog.userId], references: [users.id] }) +})); + +export const budgetAllocationsRelations = relations(budgetAllocations, ({ one }) => ({ + company: one(companies, { + fields: [budgetAllocations.companyId], + references: [companies.id] + }), + project: one(projects, { + fields: [budgetAllocations.projectId], + references: [projects.id] + }), + allocator: one(users, { fields: [budgetAllocations.allocatedBy], references: [users.id] }) +})); diff --git a/src/lib/server/db/schema/budget-allocations.ts b/src/lib/server/db/schema/budget-allocations.ts new file mode 100644 index 0000000..18e6c09 --- /dev/null +++ b/src/lib/server/db/schema/budget-allocations.ts @@ -0,0 +1,20 @@ +import { pgTable, uuid, text, numeric, timestamp } from 'drizzle-orm/pg-core'; +import { companies } from './companies.js'; +import { projects } from './projects.js'; +import { users } from './users.js'; + +export const budgetAllocations = pgTable('budget_allocations', { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + projectId: uuid('project_id') + .notNull() + .references(() => projects.id, { onDelete: 'cascade' }), + amount: numeric('amount', { precision: 15, scale: 2 }).notNull(), + allocatedBy: text('allocated_by') + .notNull() + .references(() => users.id), + note: text('note'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() +}); diff --git a/src/lib/server/db/schema/categories.ts b/src/lib/server/db/schema/categories.ts new file mode 100644 index 0000000..81a7200 --- /dev/null +++ b/src/lib/server/db/schema/categories.ts @@ -0,0 +1,16 @@ +import { pgTable, uuid, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; +import { companies } from './companies.js'; + +export const categories = pgTable( + 'categories', + { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + color: text('color'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() + }, + (table) => [uniqueIndex('categories_company_name_idx').on(table.companyId, table.name)] +); diff --git a/src/lib/server/db/schema/companies.ts b/src/lib/server/db/schema/companies.ts new file mode 100644 index 0000000..af8ab33 --- /dev/null +++ b/src/lib/server/db/schema/companies.ts @@ -0,0 +1,11 @@ +import { pgTable, uuid, text, numeric, timestamp } from 'drizzle-orm/pg-core'; + +export const companies = pgTable('companies', { + id: uuid('id').primaryKey().defaultRandom(), + name: text('name').notNull(), + description: text('description'), + totalBudget: numeric('total_budget', { precision: 15, scale: 2 }).notNull().default('0'), + currency: text('currency').notNull().default('THB'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() +}); diff --git a/src/lib/server/db/schema/company-members.ts b/src/lib/server/db/schema/company-members.ts new file mode 100644 index 0000000..1d4995a --- /dev/null +++ b/src/lib/server/db/schema/company-members.ts @@ -0,0 +1,21 @@ +import { pgTable, uuid, text, timestamp, uniqueIndex, pgEnum } from 'drizzle-orm/pg-core'; +import { users } from './users.js'; +import { companies } from './companies.js'; + +export const companyRoleEnum = pgEnum('company_role', ['admin', 'manager', 'user', 'viewer']); + +export const companyMembers = pgTable( + 'company_members', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + role: companyRoleEnum('role').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() + }, + (table) => [uniqueIndex('company_members_user_company_idx').on(table.userId, table.companyId)] +); diff --git a/src/lib/server/db/schema/expenses.ts b/src/lib/server/db/schema/expenses.ts new file mode 100644 index 0000000..50e8468 --- /dev/null +++ b/src/lib/server/db/schema/expenses.ts @@ -0,0 +1,46 @@ +import { + pgTable, + uuid, + text, + numeric, + date, + timestamp, + index, + pgEnum +} from 'drizzle-orm/pg-core'; +import { projects } from './projects.js'; +import { categories } from './categories.js'; +import { users } from './users.js'; + +export const expenseStatusEnum = pgEnum('expense_status', ['pending', 'approved', 'rejected']); + +export const expenses = pgTable( + 'expenses', + { + id: uuid('id').primaryKey().defaultRandom(), + projectId: uuid('project_id') + .notNull() + .references(() => projects.id, { onDelete: 'cascade' }), + categoryId: uuid('category_id').references(() => categories.id, { onDelete: 'set null' }), + submittedBy: text('submitted_by') + .notNull() + .references(() => users.id), + approvedBy: text('approved_by').references(() => users.id), + title: text('title').notNull(), + description: text('description'), + amount: numeric('amount', { precision: 15, scale: 2 }).notNull(), + currency: text('currency').notNull(), + receiptUrl: text('receipt_url'), + expenseDate: date('expense_date').notNull(), + status: expenseStatusEnum('status').notNull().default('pending'), + reviewedAt: timestamp('reviewed_at', { withTimezone: true }), + rejectionReason: text('rejection_reason'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() + }, + (table) => [ + index('expenses_project_status_idx').on(table.projectId, table.status), + index('expenses_submitted_by_idx').on(table.submittedBy), + index('expenses_date_idx').on(table.expenseDate) + ] +); diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts new file mode 100644 index 0000000..1f6e22c --- /dev/null +++ b/src/lib/server/db/schema/index.ts @@ -0,0 +1,9 @@ +export { users } from './users.js'; +export { sessions } from './sessions.js'; +export { companies } from './companies.js'; +export { companyRoleEnum, companyMembers } from './company-members.js'; +export { projects } from './projects.js'; +export { categories } from './categories.js'; +export { expenseStatusEnum, expenses } from './expenses.js'; +export { tags, expenseTags } from './tags.js'; +export { budgetAllocations } from './budget-allocations.js'; diff --git a/src/lib/server/db/schema/projects.ts b/src/lib/server/db/schema/projects.ts new file mode 100644 index 0000000..0ffcb0a --- /dev/null +++ b/src/lib/server/db/schema/projects.ts @@ -0,0 +1,15 @@ +import { pgTable, uuid, text, numeric, boolean, timestamp } from 'drizzle-orm/pg-core'; +import { companies } from './companies.js'; + +export const projects = pgTable('projects', { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + description: text('description'), + allocatedBudget: numeric('allocated_budget', { precision: 15, scale: 2 }).notNull().default('0'), + isActive: boolean('is_active').notNull().default(true), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() +}); diff --git a/src/lib/server/db/schema/relations.ts b/src/lib/server/db/schema/relations.ts new file mode 100644 index 0000000..06a498b --- /dev/null +++ b/src/lib/server/db/schema/relations.ts @@ -0,0 +1,83 @@ +import { relations } from 'drizzle-orm'; +import { users } from './users.js'; +import { sessions } from './sessions.js'; +import { companies } from './companies.js'; +import { companyMembers } from './company-members.js'; +import { projects } from './projects.js'; +import { categories } from './categories.js'; +import { expenses } from './expenses.js'; +import { tags, expenseTags } from './tags.js'; +import { budgetAllocations } from './budget-allocations.js'; + +export const usersRelations = relations(users, ({ many }) => ({ + sessions: many(sessions), + companyMemberships: many(companyMembers), + submittedExpenses: many(expenses, { relationName: 'submittedExpenses' }), + approvedExpenses: many(expenses, { relationName: 'approvedExpenses' }) +})); + +export const sessionsRelations = relations(sessions, ({ one }) => ({ + user: one(users, { fields: [sessions.userId], references: [users.id] }) +})); + +export const companiesRelations = relations(companies, ({ many }) => ({ + members: many(companyMembers), + projects: many(projects), + categories: many(categories), + tags: many(tags), + budgetAllocations: many(budgetAllocations) +})); + +export const companyMembersRelations = relations(companyMembers, ({ one }) => ({ + user: one(users, { fields: [companyMembers.userId], references: [users.id] }), + company: one(companies, { fields: [companyMembers.companyId], references: [companies.id] }) +})); + +export const projectsRelations = relations(projects, ({ one, many }) => ({ + company: one(companies, { fields: [projects.companyId], references: [companies.id] }), + expenses: many(expenses), + budgetAllocations: many(budgetAllocations) +})); + +export const categoriesRelations = relations(categories, ({ one, many }) => ({ + company: one(companies, { fields: [categories.companyId], references: [companies.id] }), + expenses: many(expenses) +})); + +export const expensesRelations = relations(expenses, ({ one, many }) => ({ + project: one(projects, { fields: [expenses.projectId], references: [projects.id] }), + category: one(categories, { fields: [expenses.categoryId], references: [categories.id] }), + submitter: one(users, { + fields: [expenses.submittedBy], + references: [users.id], + relationName: 'submittedExpenses' + }), + approver: one(users, { + fields: [expenses.approvedBy], + references: [users.id], + relationName: 'approvedExpenses' + }), + expenseTags: many(expenseTags) +})); + +export const tagsRelations = relations(tags, ({ one, many }) => ({ + company: one(companies, { fields: [tags.companyId], references: [companies.id] }), + expenseTags: many(expenseTags) +})); + +export const expenseTagsRelations = relations(expenseTags, ({ one }) => ({ + expense: one(expenses, { fields: [expenseTags.expenseId], references: [expenses.id] }), + tag: one(tags, { fields: [expenseTags.tagId], references: [tags.id] }) +})); + +export const budgetAllocationsRelations = relations(budgetAllocations, ({ one }) => ({ + company: one(companies, { + fields: [budgetAllocations.companyId], + references: [companies.id] + }), + project: one(projects, { + fields: [budgetAllocations.projectId], + references: [projects.id] + }), + allocator: one(users, { fields: [budgetAllocations.allocatedBy], references: [users.id] }) +})); diff --git a/src/lib/server/db/schema/sessions.ts b/src/lib/server/db/schema/sessions.ts new file mode 100644 index 0000000..fd0e9b8 --- /dev/null +++ b/src/lib/server/db/schema/sessions.ts @@ -0,0 +1,10 @@ +import { pgTable, text, timestamp } from 'drizzle-orm/pg-core'; +import { users } from './users.js'; + +export const sessions = pgTable('sessions', { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull() +}); diff --git a/src/lib/server/db/schema/tags.ts b/src/lib/server/db/schema/tags.ts new file mode 100644 index 0000000..2392886 --- /dev/null +++ b/src/lib/server/db/schema/tags.ts @@ -0,0 +1,28 @@ +import { pgTable, uuid, text, uniqueIndex, primaryKey } from 'drizzle-orm/pg-core'; +import { companies } from './companies.js'; +import { expenses } from './expenses.js'; + +export const tags = pgTable( + 'tags', + { + id: uuid('id').primaryKey().defaultRandom(), + companyId: uuid('company_id') + .notNull() + .references(() => companies.id, { onDelete: 'cascade' }), + name: text('name').notNull() + }, + (table) => [uniqueIndex('tags_company_name_idx').on(table.companyId, table.name)] +); + +export const expenseTags = pgTable( + 'expense_tags', + { + expenseId: uuid('expense_id') + .notNull() + .references(() => expenses.id, { onDelete: 'cascade' }), + tagId: uuid('tag_id') + .notNull() + .references(() => tags.id, { onDelete: 'cascade' }) + }, + (table) => [primaryKey({ columns: [table.expenseId, table.tagId] })] +); diff --git a/src/lib/server/db/schema/users.ts b/src/lib/server/db/schema/users.ts new file mode 100644 index 0000000..ae3cc4e --- /dev/null +++ b/src/lib/server/db/schema/users.ts @@ -0,0 +1,23 @@ +import { pgTable, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; + +export const users = pgTable( + 'users', + { + id: text('id').primaryKey(), + email: text('email').notNull().unique(), + username: text('username').unique(), + displayName: text('display_name'), + passwordHash: text('password_hash'), + oidcProvider: text('oidc_provider'), + oidcSubject: text('oidc_subject'), + isSystemAdmin: boolean('is_system_admin').notNull().default(false), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow() + }, + (table) => [ + uniqueIndex('users_oidc_idx') + .on(table.oidcProvider, table.oidcSubject) + .where(sql`${table.oidcProvider} IS NOT NULL AND ${table.oidcSubject} IS NOT NULL`) + ] +); diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts new file mode 100644 index 0000000..afd7de5 --- /dev/null +++ b/src/lib/types/index.ts @@ -0,0 +1,9 @@ +export type CompanyRole = 'admin' | 'manager' | 'user' | 'viewer'; +export type ExpenseStatus = 'pending' | 'approved' | 'rejected'; + +export const ROLE_HIERARCHY: Record = { + admin: 4, + manager: 3, + user: 2, + viewer: 1 +}; diff --git a/src/lib/utils/budget.ts b/src/lib/utils/budget.ts new file mode 100644 index 0000000..4510c89 --- /dev/null +++ b/src/lib/utils/budget.ts @@ -0,0 +1,12 @@ +export function budgetPercent(spent: string | number, budget: string | number): number { + const s = typeof spent === 'string' ? parseFloat(spent) : spent; + const b = typeof budget === 'string' ? parseFloat(budget) : budget; + if (b <= 0) return 0; + return Math.min((s / b) * 100, 100); +} + +export function budgetColor(pct: number): string { + if (pct > 90) return 'bg-red-500'; + if (pct > 70) return 'bg-amber-500'; + return 'bg-blue-500'; +} diff --git a/src/lib/utils/currency.ts b/src/lib/utils/currency.ts new file mode 100644 index 0000000..8285b42 --- /dev/null +++ b/src/lib/utils/currency.ts @@ -0,0 +1,18 @@ +const CURRENCY_FORMATS: Record = { + THB: { locale: 'th-TH', minimumFractionDigits: 2 }, + USD: { locale: 'en-US', minimumFractionDigits: 2 }, + EUR: { locale: 'de-DE', minimumFractionDigits: 2 } +}; + +export function formatCurrency(amount: string | number, currency: string = 'THB'): string { + const num = typeof amount === 'string' ? parseFloat(amount) : amount; + if (isNaN(num)) return `0.00 ${currency}`; + + const fmt = CURRENCY_FORMATS[currency] ?? { locale: 'en-US', minimumFractionDigits: 2 }; + + return new Intl.NumberFormat(fmt.locale, { + style: 'currency', + currency, + minimumFractionDigits: fmt.minimumFractionDigits + }).format(num); +} diff --git a/src/lib/utils/date.ts b/src/lib/utils/date.ts new file mode 100644 index 0000000..1366626 --- /dev/null +++ b/src/lib/utils/date.ts @@ -0,0 +1,16 @@ +import { format, formatDistanceToNow } from 'date-fns'; + +export function formatDate(date: string | Date): string { + const d = typeof date === 'string' ? new Date(date) : date; + return format(d, 'MMM d, yyyy'); +} + +export function formatDateTime(date: string | Date): string { + const d = typeof date === 'string' ? new Date(date) : date; + return format(d, 'MMM d, yyyy HH:mm'); +} + +export function timeAgo(date: string | Date): string { + const d = typeof date === 'string' ? new Date(date) : date; + return formatDistanceToNow(d, { addSuffix: true }); +} diff --git a/src/routes/(app)/+layout.server.ts b/src/routes/(app)/+layout.server.ts new file mode 100644 index 0000000..d92d45f --- /dev/null +++ b/src/routes/(app)/+layout.server.ts @@ -0,0 +1,27 @@ +import { redirect } from '@sveltejs/kit'; +import type { LayoutServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { companyMembers, companies } from '$lib/server/db/schema.js'; +import { eq } from 'drizzle-orm'; + +export const load: LayoutServerLoad = async ({ locals }) => { + if (!locals.user) { + redirect(302, '/login'); + } + + // Load user's companies for the sidebar + const memberships = await db + .select({ + companyId: companies.id, + companyName: companies.name, + role: companyMembers.role + }) + .from(companyMembers) + .innerJoin(companies, eq(companyMembers.companyId, companies.id)) + .where(eq(companyMembers.userId, locals.user.id)); + + return { + user: locals.user, + companies: memberships + }; +}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte new file mode 100644 index 0000000..750ceb6 --- /dev/null +++ b/src/routes/(app)/+layout.svelte @@ -0,0 +1,47 @@ + + +
+ (sidebarOpen = !sidebarOpen)} + /> + +
+ +
+ + +
+ {data.user.displayName ?? data.user.email} +
+ +
+
+
+ + +
+ {@render children()} +
+
+
diff --git a/src/routes/(app)/admin/+layout.server.ts b/src/routes/(app)/admin/+layout.server.ts new file mode 100644 index 0000000..dd3dd72 --- /dev/null +++ b/src/routes/(app)/admin/+layout.server.ts @@ -0,0 +1,7 @@ +import type { LayoutServerLoad } from './$types'; +import { requireSystemAdmin } from '$lib/server/authorization.js'; + +export const load: LayoutServerLoad = async ({ locals }) => { + requireSystemAdmin(locals); + return {}; +}; diff --git a/src/routes/(app)/admin/settings/+page.server.ts b/src/routes/(app)/admin/settings/+page.server.ts new file mode 100644 index 0000000..980896d --- /dev/null +++ b/src/routes/(app)/admin/settings/+page.server.ts @@ -0,0 +1,8 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + return { + oidcConfigured: !!(process.env.OIDC_ISSUER_URL && process.env.OIDC_CLIENT_ID), + databaseUrl: process.env.DATABASE_URL ? 'Connected' : 'Not configured' + }; +}; diff --git a/src/routes/(app)/admin/settings/+page.svelte b/src/routes/(app)/admin/settings/+page.svelte new file mode 100644 index 0000000..3f57bc8 --- /dev/null +++ b/src/routes/(app)/admin/settings/+page.svelte @@ -0,0 +1,39 @@ + + + + Settings - Admin + + +
+

System Settings

+ +
+
+

System Status

+
+
+ Database + {data.databaseUrl} +
+
+ OIDC + + {data.oidcConfigured ? 'Configured' : 'Not configured'} + +
+
+
+ +
+

Configuration

+

+ System configuration is managed via environment variables. See the .env.example file + for available options. +

+
+
+
diff --git a/src/routes/(app)/admin/users/+page.server.ts b/src/routes/(app)/admin/users/+page.server.ts new file mode 100644 index 0000000..7cbda74 --- /dev/null +++ b/src/routes/(app)/admin/users/+page.server.ts @@ -0,0 +1,38 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { users } from '$lib/server/db/schema.js'; +import { eq, sql } from 'drizzle-orm'; + +export const load: PageServerLoad = async () => { + const allUsers = await db + .select({ + id: users.id, + email: users.email, + username: users.username, + displayName: users.displayName, + isSystemAdmin: users.isSystemAdmin, + oidcProvider: users.oidcProvider, + createdAt: users.createdAt + }) + .from(users) + .orderBy(users.email); + + return { users: allUsers }; +}; + +export const actions: Actions = { + toggleAdmin: async ({ request }) => { + const formData = await request.formData(); + const userId = formData.get('userId')?.toString(); + + if (!userId) return fail(400, { error: 'User ID required' }); + + await db + .update(users) + .set({ isSystemAdmin: sql`NOT ${users.isSystemAdmin}`, updatedAt: new Date() }) + .where(eq(users.id, userId)); + + return { success: true }; + } +}; diff --git a/src/routes/(app)/admin/users/+page.svelte b/src/routes/(app)/admin/users/+page.svelte new file mode 100644 index 0000000..a864913 --- /dev/null +++ b/src/routes/(app)/admin/users/+page.svelte @@ -0,0 +1,60 @@ + + + + Users - Admin + + +
+

Manage Users

+ +
+ + + + + + + + + + + + + {#each data.users as user} + + + + + + + + + {/each} + +
NameEmailAuthAdminJoinedActions
{user.displayName ?? '—'}{user.email} + + {user.oidcProvider ? 'SSO' : 'Local'} + + + {#if user.isSystemAdmin} + Yes + {/if} + {formatDateTime(user.createdAt)} +
+ + +
+
+
+
diff --git a/src/routes/(app)/companies/+page.server.ts b/src/routes/(app)/companies/+page.server.ts new file mode 100644 index 0000000..fc1fe66 --- /dev/null +++ b/src/routes/(app)/companies/+page.server.ts @@ -0,0 +1,65 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { companies, companyMembers } from '$lib/server/db/schema.js'; +import { eq } from 'drizzle-orm'; +import { logCompanyEvent } from '$lib/server/audit.js'; +import { requireAuth } from '$lib/server/authorization.js'; + +export const load: PageServerLoad = async ({ locals }) => { + const user = requireAuth(locals); + + const userCompanies = await db + .select({ + id: companies.id, + name: companies.name, + description: companies.description, + totalBudget: companies.totalBudget, + currency: companies.currency, + role: companyMembers.role + }) + .from(companyMembers) + .innerJoin(companies, eq(companyMembers.companyId, companies.id)) + .where(eq(companyMembers.userId, user.id)); + + return { companies: userCompanies, isSystemAdmin: user.isSystemAdmin }; +}; + +export const actions: Actions = { + create: async ({ request, locals }) => { + const user = requireAuth(locals); + if (!user.isSystemAdmin) { + return fail(403, { error: 'Only system admins can create companies' }); + } + const formData = await request.formData(); + const name = formData.get('name')?.toString().trim(); + const description = formData.get('description')?.toString().trim() || null; + const currency = formData.get('currency')?.toString().trim() || 'THB'; + const totalBudget = formData.get('totalBudget')?.toString().trim() || '0'; + + if (!name) { + return fail(400, { error: 'Company name is required' }); + } + + const [company] = await db + .insert(companies) + .values({ name, description, currency, totalBudget }) + .returning(); + + // Add creator as admin of the company + await db.insert(companyMembers).values({ + userId: user.id, + companyId: company.id, + role: 'admin' + }); + + await logCompanyEvent(company.id, user.id, 'company_created', `Company "${name}" created`, { currency }); + + const budgetNum = parseFloat(totalBudget); + if (budgetNum > 0) { + await logCompanyEvent(company.id, user.id, 'budget_initial', `Initial budget set: ${totalBudget} ${currency}`, { amount: totalBudget, currency }); + } + + redirect(302, `/companies/${company.id}`); + } +}; diff --git a/src/routes/(app)/companies/+page.svelte b/src/routes/(app)/companies/+page.svelte new file mode 100644 index 0000000..5cd7c72 --- /dev/null +++ b/src/routes/(app)/companies/+page.svelte @@ -0,0 +1,138 @@ + + + + Companies - B4L Budget + + +
+
+

Companies

+ {#if data.isSystemAdmin} + + {/if} +
+ + {#if form?.error} +
{form.error}
+ {/if} + + {#if data.companies.length === 0} +
+ + + +

No companies yet

+

+ You haven't been assigned to any company yet. Ask an administrator to invite you. +

+
+ {:else} + + {/if} +
+ + +{#if showCreateModal && data.isSystemAdmin} +
+
+

Create Company

+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+{/if} diff --git a/src/routes/(app)/companies/[companyId]/+layout.server.ts b/src/routes/(app)/companies/[companyId]/+layout.server.ts new file mode 100644 index 0000000..fd690f3 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/+layout.server.ts @@ -0,0 +1,37 @@ +import { error } from '@sveltejs/kit'; +import type { LayoutServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { companies } from '$lib/server/db/schema.js'; +import { eq } from 'drizzle-orm'; +import { requireAuth, getCompanyRole } from '$lib/server/authorization.js'; + +export const load: LayoutServerLoad = async ({ locals, params }) => { + const user = requireAuth(locals); + + const [company] = await db + .select() + .from(companies) + .where(eq(companies.id, params.companyId)) + .limit(1); + + if (!company) { + error(404, 'Company not found'); + } + + const role = user.isSystemAdmin ? 'admin' : await getCompanyRole(user.id, company.id); + + if (!role) { + error(403, 'Not a member of this company'); + } + + return { + company: { + id: company.id, + name: company.name, + description: company.description, + totalBudget: company.totalBudget, + currency: company.currency + }, + companyRole: role + }; +}; diff --git a/src/routes/(app)/companies/[companyId]/+layout.svelte b/src/routes/(app)/companies/[companyId]/+layout.svelte new file mode 100644 index 0000000..d098a84 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/+layout.svelte @@ -0,0 +1,43 @@ + + +
+
+

{data.company.name}

+ {#if data.company.description} +

{data.company.description}

+ {/if} +
+ + + + + {@render children()} +
diff --git a/src/routes/(app)/companies/[companyId]/+page.server.ts b/src/routes/(app)/companies/[companyId]/+page.server.ts new file mode 100644 index 0000000..d3af353 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/+page.server.ts @@ -0,0 +1,42 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { projects, expenses } from '$lib/server/db/schema.js'; +import { eq, and, sql } from 'drizzle-orm'; + +export const load: PageServerLoad = async ({ parent }) => { + const { company } = await parent(); + + // Get projects with spent amounts + const projectList = await db + .select({ + id: projects.id, + name: projects.name, + allocatedBudget: projects.allocatedBudget, + isActive: projects.isActive, + spent: sql`coalesce(sum(case when ${expenses.status} = 'approved' then ${expenses.amount} else 0 end), 0)`, + pendingCount: sql`count(case when ${expenses.status} = 'pending' then 1 end)::int` + }) + .from(projects) + .leftJoin(expenses, eq(expenses.projectId, projects.id)) + .where(eq(projects.companyId, company.id)) + .groupBy(projects.id) + .orderBy(projects.name); + + // Recent expenses + const recentExpenses = await db + .select({ + id: expenses.id, + title: expenses.title, + amount: expenses.amount, + status: expenses.status, + expenseDate: expenses.expenseDate, + projectName: projects.name + }) + .from(expenses) + .innerJoin(projects, eq(expenses.projectId, projects.id)) + .where(eq(projects.companyId, company.id)) + .orderBy(sql`${expenses.createdAt} desc`) + .limit(10); + + return { projects: projectList, recentExpenses }; +}; diff --git a/src/routes/(app)/companies/[companyId]/+page.svelte b/src/routes/(app)/companies/[companyId]/+page.svelte new file mode 100644 index 0000000..cab356b --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/+page.svelte @@ -0,0 +1,135 @@ + + + + {data.company.name} - B4L Budget + + +
+ +
+

Remaining Budget

+
+ {formatCurrency(remaining, currency)} +
+
+
+
+

{remainingPct.toFixed(1)}% remaining

+ +
+
+ Total budget + {formatCurrency(total, currency)} +
+
+ Total spent + {formatCurrency(spent, currency)} +
+
+ Allocated + {formatCurrency(allocated, currency)} +
+
+
+ + +
+
+

Projects

+ {#if data.companyRole !== 'viewer'} + + + New Project + + {/if} +
+ + {#if data.projects.length === 0} +

No projects yet.

+ {:else} +
+ {#each data.projects as project} + {@const budgetNum = parseFloat(project.allocatedBudget)} + {@const spentNum = parseFloat(project.spent)} + {@const pct = budgetNum > 0 ? Math.min((spentNum / budgetNum) * 100, 100) : 0} + +
+ {project.name} + + {formatCurrency(project.spent, currency)} / {formatCurrency(project.allocatedBudget, currency)} + +
+
+
+
+ {#if project.pendingCount > 0} +

{project.pendingCount} pending

+ {/if} +
+ {/each} +
+ {/if} +
+ + +
+

Recent Expenses

+ {#if data.recentExpenses.length === 0} +

No expenses yet.

+ {:else} + + + + + + + + + + + + {#each data.recentExpenses as expense} + + + + + + + + {/each} + +
TitleProjectAmountDateStatus
{expense.title}{expense.projectName}{formatCurrency(expense.amount, currency)}{expense.expenseDate} + + {expense.status} + +
+ {/if} +
+
diff --git a/src/routes/(app)/companies/[companyId]/budget/+page.server.ts b/src/routes/(app)/companies/[companyId]/budget/+page.server.ts new file mode 100644 index 0000000..0f1cd8d --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/budget/+page.server.ts @@ -0,0 +1,162 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { + projects, + budgetAllocations, + companies, + users, + expenses, + companyLog +} from '$lib/server/db/schema.js'; +import { eq, sql } from 'drizzle-orm'; +import { requireCompanyRole } from '$lib/server/authorization.js'; +import { logCompanyEvent } from '$lib/server/audit.js'; +import { formatCurrency } from '$lib/utils/currency.js'; + +export const load: PageServerLoad = async ({ parent, params }) => { + const { company } = await parent(); + + const projectList = await db + .select({ + id: projects.id, + name: projects.name, + allocatedBudget: projects.allocatedBudget, + spent: sql`coalesce(sum(case when ${expenses.status} = 'approved' then ${expenses.amount} else 0 end), 0)` + }) + .from(projects) + .leftJoin(expenses, eq(expenses.projectId, projects.id)) + .where(eq(projects.companyId, params.companyId)) + .groupBy(projects.id) + .orderBy(projects.name); + + const allocations = await db + .select({ + id: budgetAllocations.id, + projectName: projects.name, + amount: budgetAllocations.amount, + allocatorName: users.displayName, + note: budgetAllocations.note, + createdAt: budgetAllocations.createdAt + }) + .from(budgetAllocations) + .innerJoin(projects, eq(budgetAllocations.projectId, projects.id)) + .innerJoin(users, eq(budgetAllocations.allocatedBy, users.id)) + .where(eq(budgetAllocations.companyId, params.companyId)) + .orderBy(sql`${budgetAllocations.createdAt} desc`) + .limit(50); + + // Changelog + const changelog = await db + .select({ + id: companyLog.id, + event: companyLog.event, + description: companyLog.description, + metadata: companyLog.metadata, + userName: users.displayName, + userEmail: users.email, + createdAt: companyLog.createdAt + }) + .from(companyLog) + .leftJoin(users, eq(companyLog.userId, users.id)) + .where(eq(companyLog.companyId, params.companyId)) + .orderBy(sql`${companyLog.createdAt} desc`) + .limit(100); + + const totalAllocated = projectList.reduce((s, p) => s + parseFloat(p.allocatedBudget), 0); + + return { projects: projectList, allocations, totalAllocated, changelog }; +}; + +export const actions: Actions = { + addBudget: async ({ request, locals, params }) => { + const { user } = await requireCompanyRole(locals, params.companyId, 'admin'); + + const formData = await request.formData(); + const amount = parseFloat(formData.get('amount')?.toString() || '0'); + + if (isNaN(amount) || amount <= 0) { + return fail(400, { error: 'Amount must be a positive number' }); + } + + // Get current budget for the log + const [company] = await db + .select({ totalBudget: companies.totalBudget, currency: companies.currency }) + .from(companies) + .where(eq(companies.id, params.companyId)) + .limit(1); + + await db + .update(companies) + .set({ + totalBudget: sql`${companies.totalBudget}::numeric + ${amount.toFixed(2)}::numeric`, + updatedAt: new Date() + }) + .where(eq(companies.id, params.companyId)); + + const newTotal = parseFloat(company.totalBudget) + amount; + await logCompanyEvent( + params.companyId, + user.id, + 'budget_added', + `Budget increased by ${formatCurrency(amount, company.currency)} (new total: ${formatCurrency(newTotal, company.currency)})`, + { amount: amount.toFixed(2), previousTotal: company.totalBudget, newTotal: newTotal.toFixed(2) } + ); + + return { success: true }; + }, + + allocate: async ({ request, locals, params }) => { + const { user } = await requireCompanyRole(locals, params.companyId, 'manager'); + + const formData = await request.formData(); + const projectId = formData.get('projectId')?.toString(); + const amount = parseFloat(formData.get('amount')?.toString() || '0'); + const note = formData.get('note')?.toString().trim() || null; + + if (!projectId || isNaN(amount) || amount === 0) { + return fail(400, { error: 'Project and non-zero amount are required' }); + } + + // Get project name and company currency for the log + const [project] = await db + .select({ name: projects.name }) + .from(projects) + .where(eq(projects.id, projectId)) + .limit(1); + + const [company] = await db + .select({ currency: companies.currency }) + .from(companies) + .where(eq(companies.id, params.companyId)) + .limit(1); + + await db + .update(projects) + .set({ + allocatedBudget: sql`${projects.allocatedBudget}::numeric + ${amount.toFixed(2)}::numeric`, + updatedAt: new Date() + }) + .where(eq(projects.id, projectId)); + + await db.insert(budgetAllocations).values({ + companyId: params.companyId, + projectId, + amount: amount.toFixed(2), + allocatedBy: user.id, + note + }); + + const event = amount > 0 ? 'budget_allocated' : 'budget_deallocated'; + const verb = amount > 0 ? 'Allocated' : 'Deallocated'; + await logCompanyEvent( + params.companyId, + user.id, + event, + `${verb} ${formatCurrency(Math.abs(amount), company.currency)} ${amount > 0 ? 'to' : 'from'} project "${project.name}"${note ? ` — ${note}` : ''}`, + { amount: amount.toFixed(2), projectId, projectName: project.name } + ); + + return { success: true }; + } +}; diff --git a/src/routes/(app)/companies/[companyId]/budget/+page.svelte b/src/routes/(app)/companies/[companyId]/budget/+page.svelte new file mode 100644 index 0000000..daf4cb8 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/budget/+page.svelte @@ -0,0 +1,263 @@ + + + + Budget - {data.company.name} + + +
+
+

Budget Allocation

+ {#if isAdmin} + + {/if} +
+ + {#if form?.error} +
{form.error}
+ {/if} + + + {#if showAddBudget && isAdmin} +
+

Replenish Company Budget

+
{ + return async ({ update }) => { + await update(); + showAddBudget = false; + }; + }} class="flex items-end gap-3"> +
+ + +
+ + +
+
+ {/if} + + +
+ +
+

Remaining Budget

+

+ {formatCurrency(remaining, currency)} +

+
+
+
+

+ {remainingPct.toFixed(1)}% of total budget remaining +

+
+ + +
+

Total Budget

+

{formatCurrency(total, currency)}

+
+ + +
+

Total Spent

+

{formatCurrency(totalSpent, currency)}

+
+ + +
+

Allocated

+

{formatCurrency(data.totalAllocated, currency)}

+
+ + +
+

Unallocated

+

+ {formatCurrency(unallocated, currency)} +

+
+
+ + + {#if canAllocate && data.projects.length > 0} +
+

Allocate Funds to Project

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ {/if} + + +
+ + + + + + + + + + + + {#each data.projects as project} + {@const allocated = parseFloat(project.allocatedBudget)} + {@const spent = parseFloat(project.spent)} + {@const remaining = allocated - spent} + {@const pct = allocated > 0 ? Math.min((spent / allocated) * 100, 100) : 0} + + + + + + + + {/each} + +
ProjectAllocatedSpentRemainingUsage
{project.name}{formatCurrency(allocated, currency)}{formatCurrency(spent, currency)}{formatCurrency(remaining, currency)} +
+
+
+
+
+ + + {#if data.changelog.length > 0} +

Activity Log

+
+
+ {#each data.changelog as entry} + {@const eventStyle = getEventStyle(entry.event)} +
+
+ {eventStyle.icon} +
+
+

{entry.description}

+

+ {entry.userName ?? entry.userEmail ?? 'System'} · {timeAgo(entry.createdAt)} +

+
+ + {eventStyle.label} + +
+ {/each} +
+
+ {/if} +
diff --git a/src/routes/(app)/companies/[companyId]/categories/+page.server.ts b/src/routes/(app)/companies/[companyId]/categories/+page.server.ts new file mode 100644 index 0000000..d2f80ba --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/categories/+page.server.ts @@ -0,0 +1,56 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { categories } from '$lib/server/db/schema.js'; +import { eq, and } from 'drizzle-orm'; +import { requireCompanyRole } from '$lib/server/authorization.js'; +import { logCompanyEvent } from '$lib/server/audit.js'; + +export const load: PageServerLoad = async ({ parent, params }) => { + await parent(); + + const categoryList = await db + .select() + .from(categories) + .where(eq(categories.companyId, params.companyId)) + .orderBy(categories.name); + + return { categories: categoryList }; +}; + +export const actions: Actions = { + create: async ({ request, locals, params }) => { + const { user } = await requireCompanyRole(locals, params.companyId, 'manager'); + + const formData = await request.formData(); + const name = formData.get('name')?.toString().trim(); + const color = formData.get('color')?.toString().trim() || '#6B7280'; + + if (!name) return fail(400, { error: 'Category name is required' }); + + await db.insert(categories).values({ + companyId: params.companyId, + name, + color + }); + + await logCompanyEvent(params.companyId, user.id, 'category_created', `Category "${name}" created`); + + return { success: true }; + }, + + delete: async ({ request, locals, params }) => { + await requireCompanyRole(locals, params.companyId, 'manager'); + + const formData = await request.formData(); + const categoryId = formData.get('categoryId')?.toString(); + + if (!categoryId) return fail(400, { error: 'Missing category ID' }); + + await db + .delete(categories) + .where(and(eq(categories.id, categoryId), eq(categories.companyId, params.companyId))); + + return { success: true }; + } +}; diff --git a/src/routes/(app)/companies/[companyId]/categories/+page.svelte b/src/routes/(app)/companies/[companyId]/categories/+page.svelte new file mode 100644 index 0000000..004b7c1 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/categories/+page.svelte @@ -0,0 +1,74 @@ + + + + Categories - {data.company.name} + + +
+

Categories

+ + {#if form?.error} +
{form.error}
+ {/if} + + {#if canManage} +
+
+ + +
+
+ + +
+ +
+ {/if} + + {#if data.categories.length === 0} +
+

No categories yet.

+
+ {:else} +
+ {#each data.categories as cat} +
+
+
+ {cat.name} +
+ {#if canManage} +
+ + +
+ {/if} +
+ {/each} +
+ {/if} +
diff --git a/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts b/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts new file mode 100644 index 0000000..f0f9cd0 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/expenses/+page.server.ts @@ -0,0 +1,122 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { expenses, projects, users, categories } from '$lib/server/db/schema.js'; +import { eq, and, sql } from 'drizzle-orm'; +import { requireCompanyRole } from '$lib/server/authorization.js'; +import { logCompanyEvent } from '$lib/server/audit.js'; +import { formatCurrency } from '$lib/utils/currency.js'; + +export const load: PageServerLoad = async ({ parent, params, url }) => { + await parent(); + + const status = url.searchParams.get('status') || 'all'; + + let query = db + .select({ + id: expenses.id, + title: expenses.title, + description: expenses.description, + amount: expenses.amount, + currency: expenses.currency, + status: expenses.status, + expenseDate: expenses.expenseDate, + rejectionReason: expenses.rejectionReason, + submittedBy: expenses.submittedBy, + submitterName: users.displayName, + submitterEmail: users.email, + projectId: projects.id, + projectName: projects.name, + categoryName: categories.name, + createdAt: expenses.createdAt + }) + .from(expenses) + .innerJoin(projects, eq(expenses.projectId, projects.id)) + .innerJoin(users, eq(expenses.submittedBy, users.id)) + .leftJoin(categories, eq(expenses.categoryId, categories.id)) + .where( + status === 'all' + ? eq(projects.companyId, params.companyId) + : and( + eq(projects.companyId, params.companyId), + eq(expenses.status, status as 'pending' | 'approved' | 'rejected') + ) + ) + .orderBy(sql`${expenses.createdAt} desc`) + .limit(100); + + const expenseList = await query; + + return { expenses: expenseList, statusFilter: status }; +}; + +export const actions: Actions = { + approve: async ({ request, locals, params }) => { + const { user } = await requireCompanyRole(locals, params.companyId, 'manager'); + const formData = await request.formData(); + const expenseId = formData.get('expenseId')?.toString(); + + if (!expenseId) return fail(400, { error: 'Missing expense ID' }); + + // Get expense details for the log + const [expense] = await db + .select({ title: expenses.title, amount: expenses.amount, currency: expenses.currency }) + .from(expenses) + .where(eq(expenses.id, expenseId)) + .limit(1); + + await db + .update(expenses) + .set({ + status: 'approved', + approvedBy: user.id, + reviewedAt: new Date(), + updatedAt: new Date() + }) + .where(and(eq(expenses.id, expenseId), eq(expenses.status, 'pending'))); + + if (expense) { + await logCompanyEvent(params.companyId, user.id, 'expense_approved', + `Approved expense "${expense.title}" for ${formatCurrency(expense.amount, expense.currency)}`, + { expenseId, amount: expense.amount } + ); + } + + return { success: true }; + }, + + reject: async ({ request, locals, params }) => { + const { user } = await requireCompanyRole(locals, params.companyId, 'manager'); + const formData = await request.formData(); + const expenseId = formData.get('expenseId')?.toString(); + const reason = formData.get('reason')?.toString().trim() || null; + + if (!expenseId) return fail(400, { error: 'Missing expense ID' }); + + const [expense] = await db + .select({ title: expenses.title, amount: expenses.amount, currency: expenses.currency }) + .from(expenses) + .where(eq(expenses.id, expenseId)) + .limit(1); + + await db + .update(expenses) + .set({ + status: 'rejected', + approvedBy: user.id, + reviewedAt: new Date(), + rejectionReason: reason, + updatedAt: new Date() + }) + .where(and(eq(expenses.id, expenseId), eq(expenses.status, 'pending'))); + + if (expense) { + await logCompanyEvent(params.companyId, user.id, 'expense_rejected', + `Rejected expense "${expense.title}" (${formatCurrency(expense.amount, expense.currency)})${reason ? ` — ${reason}` : ''}`, + { expenseId, amount: expense.amount, reason } + ); + } + + return { success: true }; + } +}; diff --git a/src/routes/(app)/companies/[companyId]/expenses/+page.svelte b/src/routes/(app)/companies/[companyId]/expenses/+page.svelte new file mode 100644 index 0000000..e826b62 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/expenses/+page.svelte @@ -0,0 +1,112 @@ + + + + Expenses - {data.company.name} + + +
+
+

Expenses

+
+ + +
+ {#each ['all', 'pending', 'approved', 'rejected'] as status} + + {status.charAt(0).toUpperCase() + status.slice(1)} + + {/each} +
+ + {#if data.expenses.length === 0} +
+

No expenses found.

+
+ {:else} +
+ {#each data.expenses as expense} +
+
+
+

{expense.title}

+

+ {expense.projectName} + {#if expense.categoryName}· {expense.categoryName}{/if} +

+ {#if expense.description} +

{expense.description}

+ {/if} +

+ By {expense.submitterName ?? expense.submitterEmail} · {expense.expenseDate} +

+
+
+

{formatCurrency(expense.amount, expense.currency)}

+ + {expense.status} + +
+
+ + {#if expense.status === 'rejected' && expense.rejectionReason} +
+ Reason: {expense.rejectionReason} +
+ {/if} + + {#if canApprove && expense.status === 'pending'} +
+
+ + +
+
+ + + +
+
+ {/if} +
+ {/each} +
+ {/if} +
diff --git a/src/routes/(app)/companies/[companyId]/import/+page.server.ts b/src/routes/(app)/companies/[companyId]/import/+page.server.ts new file mode 100644 index 0000000..a6b7e1b --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/import/+page.server.ts @@ -0,0 +1,89 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { expenses, projects, categories } from '$lib/server/db/schema.js'; +import { eq } from 'drizzle-orm'; +import { requireCompanyRole } from '$lib/server/authorization.js'; +import { logCompanyEvent } from '$lib/server/audit.js'; + +export const load: PageServerLoad = async ({ locals, params }) => { + await requireCompanyRole(locals, params.companyId, 'manager'); + + const projectList = await db + .select({ id: projects.id, name: projects.name }) + .from(projects) + .where(eq(projects.companyId, params.companyId)) + .orderBy(projects.name); + + const categoryList = await db + .select({ id: categories.id, name: categories.name }) + .from(categories) + .where(eq(categories.companyId, params.companyId)) + .orderBy(categories.name); + + return { projects: projectList, categories: categoryList }; +}; + +export const actions: Actions = { + import: async ({ request, locals, params }) => { + const { user } = await requireCompanyRole(locals, params.companyId, 'manager'); + + const formData = await request.formData(); + const jsonData = formData.get('data')?.toString(); + const projectId = formData.get('projectId')?.toString(); + const defaultStatus = formData.get('status')?.toString() || 'approved'; + + if (!jsonData || !projectId) { + return fail(400, { error: 'Data and project are required' }); + } + + let rows: Array<{ + title: string; + amount: string; + date: string; + category?: string; + description?: string; + }>; + + try { + rows = JSON.parse(jsonData); + } catch { + return fail(400, { error: 'Invalid JSON data' }); + } + + if (!Array.isArray(rows) || rows.length === 0) { + return fail(400, { error: 'No data to import' }); + } + + let imported = 0; + for (const row of rows) { + if (!row.title || !row.amount || !row.date) continue; + + const amount = parseFloat(row.amount); + if (isNaN(amount)) continue; + + await db.insert(expenses).values({ + projectId, + submittedBy: user.id, + approvedBy: defaultStatus === 'approved' ? user.id : null, + title: row.title, + description: row.description || null, + amount: Math.abs(amount).toFixed(2), + currency: 'THB', + expenseDate: row.date, + status: defaultStatus as 'pending' | 'approved' | 'rejected', + reviewedAt: defaultStatus === 'approved' ? new Date() : null + }); + imported++; + } + + if (imported > 0) { + await logCompanyEvent(params.companyId, user.id, 'import_completed', + `Imported ${imported} expenses (status: ${defaultStatus})`, + { imported, projectId, defaultStatus } + ); + } + + return { success: true, imported }; + } +}; diff --git a/src/routes/(app)/companies/[companyId]/import/+page.svelte b/src/routes/(app)/companies/[companyId]/import/+page.svelte new file mode 100644 index 0000000..1e763f4 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/import/+page.svelte @@ -0,0 +1,140 @@ + + + + Import - {data.company.name} + + +
+

Import Expenses

+

+ Paste CSV data from Actual Budget or any spreadsheet. Expected columns: title/name, amount, date. + Optional: description, category. +

+ + {#if form?.error} +
{form.error}
+ {/if} + {#if form?.success} +
+ Successfully imported {form.imported} expenses. +
+ {/if} + + +
+

1. Paste CSV Data

+ + +
+ + + {#if parsedRows.length > 0} +
+

2. Preview ({parsedRows.length} rows)

+
+ + + + {#each Object.keys(parsedRows[0]) as header} + + {/each} + + + + {#each parsedRows.slice(0, 20) as row} + + {#each Object.values(row) as val} + + {/each} + + {/each} + +
{header}
{val}
+ {#if parsedRows.length > 20} +

Showing first 20 of {parsedRows.length} rows

+ {/if} +
+
+ + +
+

3. Import

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ {/if} +
diff --git a/src/routes/(app)/companies/[companyId]/projects/+page.server.ts b/src/routes/(app)/companies/[companyId]/projects/+page.server.ts new file mode 100644 index 0000000..804d6b4 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/projects/+page.server.ts @@ -0,0 +1,27 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { projects, expenses } from '$lib/server/db/schema.js'; +import { eq, sql } from 'drizzle-orm'; + +export const load: PageServerLoad = async ({ parent }) => { + const { company } = await parent(); + + const projectList = await db + .select({ + id: projects.id, + name: projects.name, + description: projects.description, + allocatedBudget: projects.allocatedBudget, + isActive: projects.isActive, + spent: sql`coalesce(sum(case when ${expenses.status} = 'approved' then ${expenses.amount} else 0 end), 0)`, + expenseCount: sql`count(${expenses.id})::int`, + pendingCount: sql`count(case when ${expenses.status} = 'pending' then 1 end)::int` + }) + .from(projects) + .leftJoin(expenses, eq(expenses.projectId, projects.id)) + .where(eq(projects.companyId, company.id)) + .groupBy(projects.id) + .orderBy(projects.name); + + return { projects: projectList }; +}; diff --git a/src/routes/(app)/companies/[companyId]/projects/+page.svelte b/src/routes/(app)/companies/[companyId]/projects/+page.svelte new file mode 100644 index 0000000..bf02c42 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/projects/+page.svelte @@ -0,0 +1,69 @@ + + + + Projects - {data.company.name} + + +
+

Projects

+ {#if data.companyRole !== 'viewer'} + + New Project + + {/if} +
+ +{#if data.projects.length === 0} +
+

No projects yet. Create your first one.

+
+{:else} + +{/if} diff --git a/src/routes/(app)/companies/[companyId]/projects/[projectId]/+page.server.ts b/src/routes/(app)/companies/[companyId]/projects/[projectId]/+page.server.ts new file mode 100644 index 0000000..a9a5329 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/projects/[projectId]/+page.server.ts @@ -0,0 +1,50 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { projects, expenses, users, categories } from '$lib/server/db/schema.js'; +import { eq, and, sql } from 'drizzle-orm'; + +export const load: PageServerLoad = async ({ params, parent }) => { + await parent(); + + const [project] = await db + .select() + .from(projects) + .where(and(eq(projects.id, params.projectId), eq(projects.companyId, params.companyId))) + .limit(1); + + if (!project) { + error(404, 'Project not found'); + } + + const expenseList = await db + .select({ + id: expenses.id, + title: expenses.title, + description: expenses.description, + amount: expenses.amount, + currency: expenses.currency, + status: expenses.status, + expenseDate: expenses.expenseDate, + submitterName: users.displayName, + submitterEmail: users.email, + categoryName: categories.name, + createdAt: expenses.createdAt + }) + .from(expenses) + .innerJoin(users, eq(expenses.submittedBy, users.id)) + .leftJoin(categories, eq(expenses.categoryId, categories.id)) + .where(eq(expenses.projectId, params.projectId)) + .orderBy(sql`${expenses.createdAt} desc`); + + const [stats] = await db + .select({ + totalApproved: sql`coalesce(sum(case when ${expenses.status} = 'approved' then ${expenses.amount} else 0 end), 0)`, + totalPending: sql`coalesce(sum(case when ${expenses.status} = 'pending' then ${expenses.amount} else 0 end), 0)`, + count: sql`count(*)::int` + }) + .from(expenses) + .where(eq(expenses.projectId, params.projectId)); + + return { project, expenses: expenseList, stats }; +}; diff --git a/src/routes/(app)/companies/[companyId]/projects/[projectId]/+page.svelte b/src/routes/(app)/companies/[companyId]/projects/[projectId]/+page.svelte new file mode 100644 index 0000000..9887a44 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/projects/[projectId]/+page.svelte @@ -0,0 +1,92 @@ + + + + {data.project.name} - {data.company.name} + + +
+
+
+

{data.project.name}

+ {#if data.project.description} +

{data.project.description}

+ {/if} +
+ {#if canAddExpense} + + Add Expense + + {/if} +
+ + +
+
+

Budget

+

{formatCurrency(data.project.allocatedBudget, currency)}

+
+
+

Spent (Approved)

+

{formatCurrency(data.stats.totalApproved, currency)}

+
+
+

Pending

+

{formatCurrency(data.stats.totalPending, currency)}

+
+
+ + + {#if data.expenses.length === 0} +
+

No expenses yet.

+
+ {:else} +
+ + + + + + + + + + + + + {#each data.expenses as expense} + + + + + + + + + {/each} + +
TitleCategoryAmountDateSubmitted ByStatus
{expense.title}{expense.categoryName ?? '—'}{formatCurrency(expense.amount, expense.currency)}{expense.expenseDate}{expense.submitterName ?? expense.submitterEmail} + + {expense.status} + +
+
+ {/if} +
diff --git a/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.server.ts b/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.server.ts new file mode 100644 index 0000000..d0f8a5d --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.server.ts @@ -0,0 +1,98 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { expenses, categories, tags, expenseTags, projects } from '$lib/server/db/schema.js'; +import { eq, and } from 'drizzle-orm'; +import { requireCompanyRole } from '$lib/server/authorization.js'; +import { logCompanyEvent } from '$lib/server/audit.js'; +import { formatCurrency } from '$lib/utils/currency.js'; + +export const load: PageServerLoad = async ({ locals, params }) => { + await requireCompanyRole(locals, params.companyId, 'user'); + + const categoryList = await db + .select({ id: categories.id, name: categories.name, color: categories.color }) + .from(categories) + .where(eq(categories.companyId, params.companyId)) + .orderBy(categories.name); + + const tagList = await db + .select({ id: tags.id, name: tags.name }) + .from(tags) + .where(eq(tags.companyId, params.companyId)) + .orderBy(tags.name); + + // Get project info for the currency + const [project] = await db + .select({ name: projects.name }) + .from(projects) + .where(eq(projects.id, params.projectId)) + .limit(1); + + return { categories: categoryList, tags: tagList, projectName: project?.name }; +}; + +export const actions: Actions = { + default: async ({ request, locals, params }) => { + const { user } = await requireCompanyRole(locals, params.companyId, 'user'); + + const formData = await request.formData(); + const title = formData.get('title')?.toString().trim(); + const description = formData.get('description')?.toString().trim() || null; + const amount = formData.get('amount')?.toString().trim(); + const expenseDate = formData.get('expenseDate')?.toString(); + const categoryId = formData.get('categoryId')?.toString() || null; + const tagIds = formData.getAll('tagIds').map((t) => t.toString()); + + if (!title || !amount || !expenseDate) { + return fail(400, { error: 'Title, amount, and date are required' }); + } + + const parsedAmount = parseFloat(amount); + if (isNaN(parsedAmount) || parsedAmount <= 0) { + return fail(400, { error: 'Amount must be a positive number' }); + } + + // Get company currency from parent data + const [project] = await db + .select({ companyId: projects.companyId }) + .from(projects) + .where(and(eq(projects.id, params.projectId), eq(projects.companyId, params.companyId))) + .limit(1); + + if (!project) { + return fail(400, { error: 'Project not found' }); + } + + const [expense] = await db + .insert(expenses) + .values({ + projectId: params.projectId, + categoryId: categoryId || null, + submittedBy: user.id, + title, + description, + amount: parsedAmount.toFixed(2), + currency: 'THB', // Will use company currency + expenseDate + }) + .returning(); + + // Add tags + if (tagIds.length > 0) { + await db.insert(expenseTags).values( + tagIds.map((tagId) => ({ + expenseId: expense.id, + tagId + })) + ); + } + + await logCompanyEvent(params.companyId, user.id, 'expense_submitted', + `Submitted expense "${title}" for ${formatCurrency(parsedAmount, 'THB')}`, + { expenseId: expense.id, amount: parsedAmount.toFixed(2), projectId: params.projectId } + ); + + redirect(302, `/companies/${params.companyId}/projects/${params.projectId}`); + } +}; diff --git a/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.svelte b/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.svelte new file mode 100644 index 0000000..d4732f0 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/projects/[projectId]/expenses/new/+page.svelte @@ -0,0 +1,115 @@ + + + + New Expense - {data.projectName} + + +
+

Add Expense

+

Project: {data.projectName}

+ + {#if form?.error} +
{form.error}
+ {/if} + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + {#if data.tags.length > 0} +
+ +
+ {#each data.tags as tag} + + {/each} +
+
+ {/if} + +
+ + Cancel + + +
+
+
diff --git a/src/routes/(app)/companies/[companyId]/projects/new/+page.server.ts b/src/routes/(app)/companies/[companyId]/projects/new/+page.server.ts new file mode 100644 index 0000000..73bfdf0 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/projects/new/+page.server.ts @@ -0,0 +1,38 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { projects } from '$lib/server/db/schema.js'; +import { requireCompanyRole } from '$lib/server/authorization.js'; +import { logCompanyEvent } from '$lib/server/audit.js'; + +export const load: PageServerLoad = async ({ locals, params }) => { + await requireCompanyRole(locals, params.companyId, 'manager'); + return {}; +}; + +export const actions: Actions = { + default: async ({ request, locals, params }) => { + const { user } = await requireCompanyRole(locals, params.companyId, 'manager'); + + const formData = await request.formData(); + const name = formData.get('name')?.toString().trim(); + const description = formData.get('description')?.toString().trim() || null; + + if (!name) { + return fail(400, { error: 'Project name is required' }); + } + + const [project] = await db + .insert(projects) + .values({ + companyId: params.companyId, + name, + description + }) + .returning(); + + await logCompanyEvent(params.companyId, user.id, 'project_created', `Project "${name}" created`, { projectId: project.id }); + + redirect(302, `/companies/${params.companyId}/projects/${project.id}`); + } +}; diff --git a/src/routes/(app)/companies/[companyId]/projects/new/+page.svelte b/src/routes/(app)/companies/[companyId]/projects/new/+page.svelte new file mode 100644 index 0000000..339705d --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/projects/new/+page.svelte @@ -0,0 +1,56 @@ + + + + New Project + + +
+

Create Project

+ + {#if form?.error} +
{form.error}
+ {/if} + +
+
+ + +
+
+ + +
+
+ + Cancel + + +
+
+
diff --git a/src/routes/(app)/companies/[companyId]/reports/+page.server.ts b/src/routes/(app)/companies/[companyId]/reports/+page.server.ts new file mode 100644 index 0000000..5f12410 --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/reports/+page.server.ts @@ -0,0 +1,71 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { expenses, projects, categories } from '$lib/server/db/schema.js'; +import { eq, and, sql, gte, lte } from 'drizzle-orm'; + +export const load: PageServerLoad = async ({ parent, params, url }) => { + await parent(); + + const from = url.searchParams.get('from') || new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0]; + const to = url.searchParams.get('to') || new Date().toISOString().split('T')[0]; + + // Spending by category + const byCategory = await db + .select({ + categoryName: sql`coalesce(${categories.name}, 'Uncategorized')`, + categoryColor: sql`coalesce(${categories.color}, '#9CA3AF')`, + total: sql`sum(${expenses.amount})` + }) + .from(expenses) + .innerJoin(projects, eq(expenses.projectId, projects.id)) + .leftJoin(categories, eq(expenses.categoryId, categories.id)) + .where( + and( + eq(projects.companyId, params.companyId), + eq(expenses.status, 'approved'), + gte(expenses.expenseDate, from), + lte(expenses.expenseDate, to) + ) + ) + .groupBy(categories.name, categories.color); + + // Spending by project + const byProject = await db + .select({ + projectName: projects.name, + allocated: projects.allocatedBudget, + spent: sql`sum(${expenses.amount})` + }) + .from(expenses) + .innerJoin(projects, eq(expenses.projectId, projects.id)) + .where( + and( + eq(projects.companyId, params.companyId), + eq(expenses.status, 'approved'), + gte(expenses.expenseDate, from), + lte(expenses.expenseDate, to) + ) + ) + .groupBy(projects.id, projects.name, projects.allocatedBudget); + + // Spending over time (by month) + const byMonth = await db + .select({ + month: sql`to_char(${expenses.expenseDate}::date, 'YYYY-MM')`, + total: sql`sum(${expenses.amount})` + }) + .from(expenses) + .innerJoin(projects, eq(expenses.projectId, projects.id)) + .where( + and( + eq(projects.companyId, params.companyId), + eq(expenses.status, 'approved'), + gte(expenses.expenseDate, from), + lte(expenses.expenseDate, to) + ) + ) + .groupBy(sql`to_char(${expenses.expenseDate}::date, 'YYYY-MM')`) + .orderBy(sql`to_char(${expenses.expenseDate}::date, 'YYYY-MM')`); + + return { byCategory, byProject, byMonth, dateRange: { from, to } }; +}; diff --git a/src/routes/(app)/companies/[companyId]/reports/+page.svelte b/src/routes/(app)/companies/[companyId]/reports/+page.svelte new file mode 100644 index 0000000..e0e3e5f --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/reports/+page.svelte @@ -0,0 +1,139 @@ + + + + Reports - {data.company.name} + + +
+

Reports

+ + +
+
+ + +
+
+ + +
+ +
+ +
+ +
+

Spending by Category

+ {#if data.byCategory.length === 0} +

No data for this period.

+ {:else} +
+ {#each data.byCategory as cat} + {@const total = data.byCategory.reduce((s, c) => s + parseFloat(c.total), 0)} + {@const pct = total > 0 ? (parseFloat(cat.total) / total) * 100 : 0} +
+
+
+
+ {cat.categoryName} +
+ {formatCurrency(cat.total, currency)} +
+
+
+
+
+ {/each} +
+ {/if} +
+ + +
+

Budget vs Actual by Project

+ {#if data.byProject.length === 0} +

No data for this period.

+ {:else} +
+ {#each data.byProject as project} + {@const allocated = parseFloat(project.allocated)} + {@const spent = parseFloat(project.spent)} + {@const pct = allocated > 0 ? Math.min((spent / allocated) * 100, 100) : 0} +
+
+ {project.projectName} + + {formatCurrency(spent, currency)} / {formatCurrency(allocated, currency)} + +
+
+
+
+
+
+
+ {/each} +
+ {/if} +
+ + +
+

Monthly Spending

+ {#if data.byMonth.length === 0} +

No data for this period.

+ {:else} + {@const maxVal = Math.max(...data.byMonth.map((m) => parseFloat(m.total)))} +
+ {#each data.byMonth as month} + {@const val = parseFloat(month.total)} + {@const height = maxVal > 0 ? (val / maxVal) * 100 : 0} +
+ {formatCurrency(val, currency)} +
+ {month.month} +
+ {/each} +
+ {/if} +
+
+
diff --git a/src/routes/(app)/companies/[companyId]/settings/+page.server.ts b/src/routes/(app)/companies/[companyId]/settings/+page.server.ts new file mode 100644 index 0000000..5270d2c --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/settings/+page.server.ts @@ -0,0 +1,149 @@ +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { companyMembers, companies, users } from '$lib/server/db/schema.js'; +import { eq, and } from 'drizzle-orm'; +import { requireCompanyRole } from '$lib/server/authorization.js'; +import type { CompanyRole } from '$lib/types/index.js'; +import { logCompanyEvent } from '$lib/server/audit.js'; + +export const load: PageServerLoad = async ({ locals, params }) => { + await requireCompanyRole(locals, params.companyId, 'manager'); + + const members = await db + .select({ + id: companyMembers.id, + userId: users.id, + email: users.email, + displayName: users.displayName, + role: companyMembers.role + }) + .from(companyMembers) + .innerJoin(users, eq(companyMembers.userId, users.id)) + .where(eq(companyMembers.companyId, params.companyId)) + .orderBy(users.email); + + return { members }; +}; + +export const actions: Actions = { + updateCompany: async ({ request, locals, params }) => { + const { user } = await requireCompanyRole(locals, params.companyId, 'admin'); + + const formData = await request.formData(); + const name = formData.get('name')?.toString().trim(); + const description = formData.get('description')?.toString().trim() || null; + + if (!name) return fail(400, { error: 'Company name is required' }); + + await db + .update(companies) + .set({ name, description, updatedAt: new Date() }) + .where(eq(companies.id, params.companyId)); + + await logCompanyEvent(params.companyId, user.id, 'company_updated', `Company details updated (name: "${name}")`); + + return { success: true, message: 'Company updated' }; + }, + + addMember: async ({ request, locals, params }) => { + const { user: admin } = await requireCompanyRole(locals, params.companyId, 'admin'); + + const formData = await request.formData(); + const email = formData.get('email')?.toString().trim().toLowerCase(); + const role = formData.get('role')?.toString() as CompanyRole; + + if (!email || !role) return fail(400, { error: 'Email and role are required' }); + + const [targetUser] = await db + .select({ id: users.id, displayName: users.displayName }) + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (!targetUser) return fail(400, { error: 'User not found. They must sign up first.' }); + + const existing = await db + .select({ id: companyMembers.id }) + .from(companyMembers) + .where(and(eq(companyMembers.userId, targetUser.id), eq(companyMembers.companyId, params.companyId))) + .limit(1); + + if (existing.length > 0) return fail(400, { error: 'User is already a member' }); + + await db.insert(companyMembers).values({ + userId: targetUser.id, + companyId: params.companyId, + role + }); + + await logCompanyEvent(params.companyId, admin.id, 'member_added', + `Added ${targetUser.displayName ?? email} as ${role}`, + { targetUserId: targetUser.id, email, role } + ); + + return { success: true }; + }, + + updateRole: async ({ request, locals, params }) => { + const { user: admin } = await requireCompanyRole(locals, params.companyId, 'admin'); + + const formData = await request.formData(); + const memberId = formData.get('memberId')?.toString(); + const role = formData.get('role')?.toString() as CompanyRole; + + if (!memberId || !role) return fail(400, { error: 'Member and role are required' }); + + // Get member info for the log + const [member] = await db + .select({ userId: companyMembers.userId, oldRole: companyMembers.role, email: users.email, displayName: users.displayName }) + .from(companyMembers) + .innerJoin(users, eq(companyMembers.userId, users.id)) + .where(and(eq(companyMembers.id, memberId), eq(companyMembers.companyId, params.companyId))) + .limit(1); + + await db + .update(companyMembers) + .set({ role }) + .where(and(eq(companyMembers.id, memberId), eq(companyMembers.companyId, params.companyId))); + + if (member) { + await logCompanyEvent(params.companyId, admin.id, 'member_role_changed', + `Changed ${member.displayName ?? member.email} role from ${member.oldRole} to ${role}`, + { targetUserId: member.userId, oldRole: member.oldRole, newRole: role } + ); + } + + return { success: true }; + }, + + removeMember: async ({ request, locals, params }) => { + const { user: admin } = await requireCompanyRole(locals, params.companyId, 'admin'); + + const formData = await request.formData(); + const memberId = formData.get('memberId')?.toString(); + + if (!memberId) return fail(400, { error: 'Member ID required' }); + + // Get member info for the log + const [member] = await db + .select({ userId: companyMembers.userId, email: users.email, displayName: users.displayName, role: companyMembers.role }) + .from(companyMembers) + .innerJoin(users, eq(companyMembers.userId, users.id)) + .where(and(eq(companyMembers.id, memberId), eq(companyMembers.companyId, params.companyId))) + .limit(1); + + await db + .delete(companyMembers) + .where(and(eq(companyMembers.id, memberId), eq(companyMembers.companyId, params.companyId))); + + if (member) { + await logCompanyEvent(params.companyId, admin.id, 'member_removed', + `Removed ${member.displayName ?? member.email} (was ${member.role})`, + { targetUserId: member.userId, role: member.role } + ); + } + + return { success: true }; + } +}; diff --git a/src/routes/(app)/companies/[companyId]/settings/+page.svelte b/src/routes/(app)/companies/[companyId]/settings/+page.svelte new file mode 100644 index 0000000..c81552c --- /dev/null +++ b/src/routes/(app)/companies/[companyId]/settings/+page.svelte @@ -0,0 +1,135 @@ + + + + Settings - {data.company.name} + + +
+ {#if form?.error} +
{form.error}
+ {/if} + {#if form?.message} +
{form.message}
+ {/if} + + + {#if isAdmin} +
+

Company Details

+
+
+ + +
+
+ + +
+

+ To add budget, go to the Budget page and use the "+ Add Budget" button. +

+ +
+
+ {/if} + + +
+

Members

+ + {#if isAdmin} +
+
+ + +
+
+ + +
+ +
+ {/if} + + + + + + + + {#if isAdmin} + + {/if} + + + + {#each data.members as member} + + + + + {#if isAdmin} + + {/if} + + {/each} + +
UserEmailRoleActions
{member.displayName ?? '—'}{member.email} + {#if isAdmin} +
+ + +
+ {:else} + {member.role} + {/if} +
+
+ + +
+
+
+
diff --git a/src/routes/(app)/dashboard/+page.server.ts b/src/routes/(app)/dashboard/+page.server.ts new file mode 100644 index 0000000..d1eda16 --- /dev/null +++ b/src/routes/(app)/dashboard/+page.server.ts @@ -0,0 +1,61 @@ +import type { PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { + companyMembers, + companies, + projects, + expenses +} from '$lib/server/db/schema.js'; +import { eq, and, sql } from 'drizzle-orm'; + +export const load: PageServerLoad = async ({ locals }) => { + const userId = locals.user!.id; + + // Get all companies the user belongs to with summary stats + const userCompanies = await db + .select({ + id: companies.id, + name: companies.name, + totalBudget: companies.totalBudget, + currency: companies.currency, + role: companyMembers.role + }) + .from(companyMembers) + .innerJoin(companies, eq(companyMembers.companyId, companies.id)) + .where(eq(companyMembers.userId, userId)); + + // For each company, get project count and pending expense count + const companySummaries = await Promise.all( + userCompanies.map(async (company) => { + const [projectCount] = await db + .select({ count: sql`count(*)::int` }) + .from(projects) + .where(eq(projects.companyId, company.id)); + + const [pendingCount] = await db + .select({ count: sql`count(*)::int` }) + .from(expenses) + .innerJoin(projects, eq(expenses.projectId, projects.id)) + .where( + and(eq(projects.companyId, company.id), eq(expenses.status, 'pending')) + ); + + const [approvedTotal] = await db + .select({ total: sql`coalesce(sum(${expenses.amount}), 0)` }) + .from(expenses) + .innerJoin(projects, eq(expenses.projectId, projects.id)) + .where( + and(eq(projects.companyId, company.id), eq(expenses.status, 'approved')) + ); + + return { + ...company, + projectCount: projectCount.count, + pendingExpenses: pendingCount.count, + totalSpent: approvedTotal.total + }; + }) + ); + + return { companySummaries }; +}; diff --git a/src/routes/(app)/dashboard/+page.svelte b/src/routes/(app)/dashboard/+page.svelte new file mode 100644 index 0000000..f2d554b --- /dev/null +++ b/src/routes/(app)/dashboard/+page.svelte @@ -0,0 +1,86 @@ + + + + Dashboard - B4L Budget + + +
+
+

Dashboard

+ {#if data.user?.isSystemAdmin} + + Manage Companies + + {/if} +
+ + {#if data.companySummaries.length === 0} +
+ + + +

Waiting for access

+

+ You haven't been assigned to any company yet. Ask an administrator to invite you. +

+
+ {:else} + + {/if} +
diff --git a/src/routes/(auth)/+layout.svelte b/src/routes/(auth)/+layout.svelte new file mode 100644 index 0000000..eebb4a7 --- /dev/null +++ b/src/routes/(auth)/+layout.svelte @@ -0,0 +1,9 @@ + + +
+
+ {@render children()} +
+
diff --git a/src/routes/(auth)/login/+page.server.ts b/src/routes/(auth)/login/+page.server.ts new file mode 100644 index 0000000..be85da3 --- /dev/null +++ b/src/routes/(auth)/login/+page.server.ts @@ -0,0 +1,54 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { users } from '$lib/server/db/schema.js'; +import { eq } from 'drizzle-orm'; +import { verifyPassword } from '$lib/server/auth/password.js'; +import { + generateSessionToken, + createSession, + setSessionCookie +} from '$lib/server/auth/index.js'; +import { isOIDCEnabled } from '$lib/server/auth/oidc.js'; + +export const load: PageServerLoad = async ({ locals }) => { + if (locals.user) { + redirect(302, '/dashboard'); + } + return { oidcEnabled: isOIDCEnabled() }; +}; + +export const actions: Actions = { + default: async (event) => { + const formData = await event.request.formData(); + const email = formData.get('email')?.toString().trim().toLowerCase(); + const password = formData.get('password')?.toString(); + + if (!email || !password) { + return fail(400, { error: 'Email and password are required', email }); + } + + const result = await db + .select() + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (result.length === 0 || !result[0].passwordHash) { + return fail(400, { error: 'Invalid email or password', email }); + } + + const user = result[0]; + const valid = await verifyPassword(user.passwordHash!, password); + + if (!valid) { + return fail(400, { error: 'Invalid email or password', email }); + } + + const token = generateSessionToken(); + const session = await createSession(token, user.id); + setSessionCookie(event, token, session.expiresAt); + + redirect(302, '/dashboard'); + } +}; diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte new file mode 100644 index 0000000..33e0249 --- /dev/null +++ b/src/routes/(auth)/login/+page.svelte @@ -0,0 +1,76 @@ + + + + Login - Buildfor Life Budget + + +
+

Sign In

+ + {#if form?.error} +
+ {form.error} +
+ {/if} + +
+
+ + +
+ +
+ + +
+ + +
+ + {#if data.oidcEnabled} +
+
+
+
+
+
+ Or continue with +
+
+ + Single Sign-On (SSO) + +
+ {/if} + +

+ Don't have an account? + Sign up +

+
diff --git a/src/routes/(auth)/logout/+page.server.ts b/src/routes/(auth)/logout/+page.server.ts new file mode 100644 index 0000000..ee508f3 --- /dev/null +++ b/src/routes/(auth)/logout/+page.server.ts @@ -0,0 +1,18 @@ +import { redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { invalidateSession, deleteSessionCookie } from '$lib/server/auth/index.js'; + +export const load: PageServerLoad = async () => { + redirect(302, '/login'); +}; + +export const actions: Actions = { + default: async (event) => { + const token = event.cookies.get('session'); + if (token) { + await invalidateSession(token); + } + deleteSessionCookie(event); + redirect(302, '/login'); + } +}; diff --git a/src/routes/(auth)/oidc/+server.ts b/src/routes/(auth)/oidc/+server.ts new file mode 100644 index 0000000..202bb7f --- /dev/null +++ b/src/routes/(auth)/oidc/+server.ts @@ -0,0 +1,34 @@ +import { redirect } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { + generateState, + generateCodeVerifier, + getAuthorizationUrl, + isOIDCEnabled +} from '$lib/server/auth/oidc.js'; + +export const GET: RequestHandler = async ({ cookies }) => { + if (!isOIDCEnabled()) { + redirect(302, '/login'); + } + + const state = generateState(); + const codeVerifier = generateCodeVerifier(); + + cookies.set('oidc_state', state, { + httpOnly: true, + sameSite: 'lax', + path: '/', + maxAge: 600 // 10 minutes + }); + + cookies.set('oidc_code_verifier', codeVerifier, { + httpOnly: true, + sameSite: 'lax', + path: '/', + maxAge: 600 + }); + + const url = await getAuthorizationUrl(state, codeVerifier); + redirect(302, url); +}; diff --git a/src/routes/(auth)/oidc/callback/+server.ts b/src/routes/(auth)/oidc/callback/+server.ts new file mode 100644 index 0000000..4bf6267 --- /dev/null +++ b/src/routes/(auth)/oidc/callback/+server.ts @@ -0,0 +1,92 @@ +import { error, redirect } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { exchangeCode, getUserInfo, getOIDCConfig } from '$lib/server/auth/oidc.js'; +import { db } from '$lib/server/db/index.js'; +import { users } from '$lib/server/db/schema.js'; +import { and, eq } from 'drizzle-orm'; +import { + generateSessionToken, + generateUserId, + createSession, + setSessionCookie +} from '$lib/server/auth/index.js'; + +export const GET: RequestHandler = async (event) => { + const { url, cookies } = event; + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const storedState = cookies.get('oidc_state'); + const codeVerifier = cookies.get('oidc_code_verifier'); + + // Clean up cookies + cookies.delete('oidc_state', { path: '/' }); + cookies.delete('oidc_code_verifier', { path: '/' }); + + if (!code || !state || !storedState || !codeVerifier) { + error(400, 'Missing OIDC parameters'); + } + + if (state !== storedState) { + error(400, 'Invalid OIDC state'); + } + + const { accessToken } = await exchangeCode(code, codeVerifier); + const userInfo = await getUserInfo(accessToken); + const config = await getOIDCConfig(); + + // Find existing user by OIDC identity + let user = await db + .select() + .from(users) + .where( + and(eq(users.oidcProvider, config.issuerUrl), eq(users.oidcSubject, userInfo.sub)) + ) + .limit(1) + .then((r) => r[0] ?? null); + + if (!user) { + // Check if a user with this email exists (link accounts) + if (userInfo.email) { + user = await db + .select() + .from(users) + .where(eq(users.email, userInfo.email)) + .limit(1) + .then((r) => r[0] ?? null); + + if (user) { + // Link OIDC identity to existing user + await db + .update(users) + .set({ + oidcProvider: config.issuerUrl, + oidcSubject: userInfo.sub, + updatedAt: new Date() + }) + .where(eq(users.id, user.id)); + } + } + + if (!user) { + // Create new user + const userId = generateUserId(); + const result = await db + .insert(users) + .values({ + id: userId, + email: userInfo.email, + displayName: userInfo.name || userInfo.email, + oidcProvider: config.issuerUrl, + oidcSubject: userInfo.sub + }) + .returning(); + user = result[0]; + } + } + + const token = generateSessionToken(); + const session = await createSession(token, user.id); + setSessionCookie(event, token, session.expiresAt); + + redirect(302, '/dashboard'); +}; diff --git a/src/routes/(auth)/signup/+page.server.ts b/src/routes/(auth)/signup/+page.server.ts new file mode 100644 index 0000000..8401737 --- /dev/null +++ b/src/routes/(auth)/signup/+page.server.ts @@ -0,0 +1,68 @@ +import { fail, redirect } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; +import { db } from '$lib/server/db/index.js'; +import { users } from '$lib/server/db/schema.js'; +import { eq } from 'drizzle-orm'; +import { hashPassword } from '$lib/server/auth/password.js'; +import { + generateSessionToken, + generateUserId, + createSession, + setSessionCookie +} from '$lib/server/auth/index.js'; + +export const load: PageServerLoad = async ({ locals }) => { + if (locals.user) { + redirect(302, '/dashboard'); + } + return {}; +}; + +export const actions: Actions = { + default: async (event) => { + const formData = await event.request.formData(); + const displayName = formData.get('displayName')?.toString().trim(); + const email = formData.get('email')?.toString().trim().toLowerCase(); + const password = formData.get('password')?.toString(); + const confirmPassword = formData.get('confirmPassword')?.toString(); + + if (!displayName || !email || !password || !confirmPassword) { + return fail(400, { error: 'All fields are required', displayName, email }); + } + + if (password.length < 8) { + return fail(400, { error: 'Password must be at least 8 characters', displayName, email }); + } + + if (password !== confirmPassword) { + return fail(400, { error: 'Passwords do not match', displayName, email }); + } + + // Check if email already exists + const existing = await db + .select({ id: users.id }) + .from(users) + .where(eq(users.email, email)) + .limit(1); + + if (existing.length > 0) { + return fail(400, { error: 'An account with this email already exists', displayName, email }); + } + + const userId = generateUserId(); + const passwordHash = await hashPassword(password); + + await db.insert(users).values({ + id: userId, + email, + displayName, + passwordHash + }); + + const token = generateSessionToken(); + const session = await createSession(token, userId); + setSessionCookie(event, token, session.expiresAt); + + redirect(302, '/dashboard'); + } +}; diff --git a/src/routes/(auth)/signup/+page.svelte b/src/routes/(auth)/signup/+page.svelte new file mode 100644 index 0000000..d3d301f --- /dev/null +++ b/src/routes/(auth)/signup/+page.svelte @@ -0,0 +1,86 @@ + + + + Sign Up - Buildfor Life Budget + + +
+

Create Account

+ + {#if form?.error} +
+ {form.error} +
+ {/if} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +

+ Already have an account? + Sign in +

+
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 0000000..ec238fd --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,7 @@ +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async ({ locals }) => { + return { + user: locals.user + }; +}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..b93e9ba --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..fff24f9 --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,9 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ locals }) => { + if (locals.user) { + redirect(302, '/dashboard'); + } + redirect(302, '/login'); +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..a7b4935 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,15 @@ + + + + Buildfor Life Budget + + +{#if data.user} + +{:else} + +{/if} diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000..e69de29 diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..d1fedcb --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,15 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter({ + out: 'build', + precompress: true + }) + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a8f10c8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..2d35c4f --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()] +});