From 3be4e1bf7de7c7585755ab07de5674a77d5af157 Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Fri, 7 Nov 2025 19:26:32 +0100 Subject: [PATCH] Rewritten to use drizzle instead of prisma commit c0894548dac5133bd89da5b68684443748fa2559 Author: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Fri Nov 7 18:38:30 2025 +0100 Update config.ts commit 5a4f1159d2123ada0f698a10011c24720bf6ea6f Author: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Fri Nov 7 15:58:13 2025 +0100 first drizzle rewrite --- README.md | 91 +- app/(dashboard)/page.tsx | 52 +- docker-compose.yml | 12 +- docker/web/Dockerfile | 26 +- docker/web/entrypoint.sh | 46 +- drizzle.config.ts | 10 + drizzle/0000_initial.sql | 152 +++ drizzle/meta/0001_snapshot.json | 1046 +++++++++++++++++++++ drizzle/meta/_journal.json | 13 + package-lock.json | 1510 ++++++++++++++++++++++-------- package.json | 7 +- prisma/schema.prisma | 206 ---- src/lib/audit.ts | 15 +- src/lib/auth.ts | 7 +- src/lib/caddy.ts | 142 +-- src/lib/config.ts | 117 ++- src/lib/db.ts | 130 ++- src/lib/db/schema.ts | 175 ++++ src/lib/init-db.ts | 48 +- src/lib/models/access-lists.ts | 201 ++-- src/lib/models/audit.ts | 17 +- src/lib/models/certificates.ts | 65 +- src/lib/models/dead-hosts.ts | 60 +- src/lib/models/proxy-hosts.ts | 67 +- src/lib/models/redirect-hosts.ts | 61 +- src/lib/models/user.ts | 100 +- src/lib/settings.ts | 30 +- 27 files changed, 3258 insertions(+), 1148 deletions(-) mode change 100755 => 100644 docker/web/entrypoint.sh create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_initial.sql create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/_journal.json delete mode 100644 prisma/schema.prisma create mode 100644 src/lib/db/schema.ts diff --git a/README.md b/README.md index 511fb601..7ffdc83a 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Caddy Proxy Manager brings a beautiful, intuitive web interface to [Caddy Server - **Hardened by default** – Login throttling, strict session management, HSTS injection - **Admin-first design** – Single admin account with production credential enforcement - **Secure secrets** – API tokens never displayed after initial entry, restrictive file permissions -- **Modern stack** – Built on Next.js 16, React 19, and Prisma with TypeScript throughout +- **Modern stack** – Built on Next.js 16, React 19, and Drizzle ORM with TypeScript throughout --- @@ -125,7 +125,7 @@ Visit `http://localhost:3000/login` and sign in with your credentials. - **Next.js 16 App Router** – Server components, streaming, and server actions - **Material UI Components** – Responsive design with dark theme - **Direct Caddy Integration** – Generates JSON config and pushes via Caddy Admin API -- **Prisma ORM** – Type-safe database access with automatic migrations +- **Drizzle ORM** – Type-safe SQLite access with checked-in SQL migrations - **SQLite Database** – Zero-configuration persistence with full ACID compliance - **Cloudflare DNS-01** – Automated wildcard certificate issuance - **bcrypt Authentication** – Industry-standard password hashing for access lists @@ -138,18 +138,37 @@ Visit `http://localhost:3000/login` and sign in with your credentials. | Variable | Description | Default | Required | |----------|-------------|---------|----------| -| `ADMIN_USERNAME` | Admin login username | `admin` (dev only) | Yes (production) | -| `ADMIN_PASSWORD` | Admin password (min 12 chars with letters & numbers) | `admin` (dev only) | Yes (production) | -| `SESSION_SECRET` | 32+ character string for session signing | - | Yes | +| `SESSION_SECRET` | Session encryption key (32+ chars) | None | **Yes** | +| `ADMIN_USERNAME` | Admin login username | `admin` | **Yes** | +| `ADMIN_PASSWORD` | Admin password (see requirements below) | `admin` (dev only) | **Yes** | | `BASE_URL` | Public URL of the dashboard | `http://localhost:3000` | No | | `CADDY_API_URL` | Caddy Admin API endpoint | `http://caddy:2019` | No | | `DATABASE_PATH` | SQLite file path | `/app/data/caddy-proxy-manager.db` | No | | `PRIMARY_DOMAIN` | Default domain for Caddy config | `caddyproxymanager.com` | No | -**Production Security Requirements:** -- `ADMIN_PASSWORD` must be 12+ characters with both letters and numbers -- `SESSION_SECRET` must be 32+ characters and not a default value -- Default credentials (`admin`/`admin`) are automatically rejected +**Production Security Requirements (Strictly Enforced):** + +The application will **fail to start** in production if these requirements are not met: + +- **`SESSION_SECRET`**: + - Must be at least 32 characters long + - Cannot be a known placeholder value + - Generate with: `openssl rand -base64 32` + +- **`ADMIN_USERNAME`**: + - Must be set (any value is acceptable, including `admin`) + +- **`ADMIN_PASSWORD`**: + - Minimum 12 characters + - Must include uppercase letters (A-Z) + - Must include lowercase letters (a-z) + - Must include numbers (0-9) + - Must include special characters (!@#$%^&* etc.) + - Cannot be `admin` in production + +**Development Mode:** +- Default credentials (`admin`/`admin`) are allowed in development +- Set `NODE_ENV=development` to use relaxed validation --- @@ -167,7 +186,7 @@ caddy-proxy-manager/ │ ├── models/ # Database models and operations │ ├── caddy/ # Caddy config generation │ └── auth/ # Authentication helpers -├── prisma/ # Database schema and migrations +├── drizzle/ # Database migrations ├── docker/ │ ├── web/ # Next.js production Dockerfile │ └── caddy/ # Custom Caddy build (xcaddy + modules) @@ -181,18 +200,56 @@ caddy-proxy-manager/ We take security seriously. Here's what's built-in: -- **Session Secret Enforcement** – Production requires strong, unique session secrets -- **Credential Validation** – Default credentials rejected; minimum complexity enforced +### Authentication & Authorization +- **Strict Credential Enforcement** – Application refuses to start in production with weak/default credentials +- **Password Complexity** – Enforced minimum 12 chars with uppercase, lowercase, numbers, and special characters +- **Session Secret Validation** – 32+ character secrets required with automatic detection of insecure placeholders - **Login Throttling** – IP + username based rate limiting (5 attempts / 5 minutes) - **Admin-Only Mutations** – All configuration changes require admin privileges +- **Fail-Fast Validation** – Security checks run at server startup, not at first request + +### Data Protection - **Certificate Protection** – Imported certificates stored with `0600` permissions -- **Audit Trail** – Immutable log of all administrative actions -- **HSTS Headers** – Strict-Transport-Security automatically applied to managed hosts +- **Session Encryption** – All sessions encrypted with validated secrets - **Secret Redaction** – API tokens never rendered back to the browser +- **Audit Trail** – Immutable log of all administrative actions + +### Infrastructure Security +- **HSTS Headers** – Strict-Transport-Security automatically applied to managed hosts +- **Secure Defaults** – All security features enabled by default +- **Docker Security** – Minimal attack surface with multi-stage builds +- **Privilege Dropping** – Containers run as non-root users + +### Security Best Practices + +**For Production Deployments:** +```bash +# 1. Generate a secure session secret +export SESSION_SECRET=$(openssl rand -base64 32) + +# 2. Create strong admin credentials +export ADMIN_USERNAME="admin" # Any username is fine +export ADMIN_PASSWORD="Your-Str0ng-P@ssw0rd!" # 12+ chars, mixed case, numbers, special chars + +# 3. Store credentials securely +echo "SESSION_SECRET=$SESSION_SECRET" > .env +echo "ADMIN_USERNAME=$ADMIN_USERNAME" >> .env +echo "ADMIN_PASSWORD=$ADMIN_PASSWORD" >> .env +chmod 600 .env # Restrict file permissions +``` + +**For Development:** +```bash +# Development mode allows default credentials +export NODE_ENV=development +npm run dev +# Login with admin/admin +``` **Known Limitations:** - Imported certificate keys stored in SQLite without encryption (planned enhancement) - In-memory rate limiting (requires Redis/Memcached for multi-instance deployments) +- No 2FA support yet (planned enhancement) --- @@ -223,7 +280,7 @@ To enable automatic SSL certificates with Cloudflare DNS-01 challenges: ### Development Notes -- **Database:** Prisma manages schema migrations. Run `npx prisma db push` to sync changes. +- **Database:** Drizzle migrations live in `/drizzle`. Run `npm run db:migrate` to apply them to your local SQLite file. - **Caddy Config:** Rebuilt on each mutation and pushed to Caddy Admin API. Errors are surfaced in the UI. - **Rate Limiting:** Kept in-memory per Node process. For horizontal scaling, use Redis/Memcached. - **Authentication:** Currently supports single admin user. Multi-role support requires architecture changes. @@ -234,8 +291,6 @@ To enable automatic SSL certificates with Cloudflare DNS-01 challenges: We're actively working on these improvements: -- [ ] Encrypted storage for imported certificate private keys -- [ ] Redis/Memcached integration for distributed rate limiting - [ ] Multi-user support with role-based access control - [ ] Additional DNS providers (Namecheap, Route53, etc.) - [ ] Metrics and monitoring dashboard @@ -287,7 +342,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file - **[Nginx Proxy Manager](https://github.com/NginxProxyManager/nginx-proxy-manager)** – The original project - **[Next.js](https://nextjs.org/)** – React framework for production - **[Material UI](https://mui.com/)** – Beautiful React components -- **[Prisma](https://www.prisma.io/)** – Next-generation ORM +- **[Drizzle ORM](https://orm.drizzle.team/)** – Lightweight SQL migrations and type-safe queries --- diff --git a/app/(dashboard)/page.tsx b/app/(dashboard)/page.tsx index 8081f920..67990643 100644 --- a/app/(dashboard)/page.tsx +++ b/app/(dashboard)/page.tsx @@ -1,6 +1,15 @@ -import prisma from "@/src/lib/db"; +import db, { toIso } from "@/src/lib/db"; import { requireUser } from "@/src/lib/auth"; import OverviewClient from "./OverviewClient"; +import { + accessLists, + auditEvents, + certificates, + deadHosts, + proxyHosts, + redirectHosts +} from "@/src/lib/db/schema"; +import { count, desc } from "drizzle-orm"; type StatCard = { label: string; @@ -10,14 +19,19 @@ type StatCard = { }; async function loadStats(): Promise { - const [proxyHostsCount, redirectHostsCount, deadHostsCount, certificatesCount, accessListsCount] = + const [proxyHostCountResult, redirectHostCountResult, deadHostCountResult, certificateCountResult, accessListCountResult] = await Promise.all([ - prisma.proxyHost.count(), - prisma.redirectHost.count(), - prisma.deadHost.count(), - prisma.certificate.count(), - prisma.accessList.count() + db.select({ value: count() }).from(proxyHosts), + db.select({ value: count() }).from(redirectHosts), + db.select({ value: count() }).from(deadHosts), + db.select({ value: count() }).from(certificates), + db.select({ value: count() }).from(accessLists) ]); + const proxyHostsCount = proxyHostCountResult[0]?.value ?? 0; + const redirectHostsCount = redirectHostCountResult[0]?.value ?? 0; + const deadHostsCount = deadHostCountResult[0]?.value ?? 0; + const certificatesCount = certificateCountResult[0]?.value ?? 0; + const accessListsCount = accessListCountResult[0]?.value ?? 0; return [ { label: "Proxy Hosts", icon: "⇄", count: proxyHostsCount, href: "/proxy-hosts" }, @@ -31,24 +45,24 @@ async function loadStats(): Promise { export default async function OverviewPage() { const session = await requireUser(); const stats = await loadStats(); - const recentEvents = await prisma.auditEvent.findMany({ - select: { - action: true, - entityType: true, - summary: true, - createdAt: true - }, - orderBy: { createdAt: "desc" }, - take: 8 - }); + const recentEvents = await db + .select({ + action: auditEvents.action, + entityType: auditEvents.entityType, + summary: auditEvents.summary, + createdAt: auditEvents.createdAt + }) + .from(auditEvents) + .orderBy(desc(auditEvents.createdAt)) + .limit(8); return ( ({ + recentEvents={recentEvents.map((event) => ({ summary: event.summary ?? `${event.action} on ${event.entityType}`, - created_at: event.createdAt.toISOString() + created_at: toIso(event.createdAt)! }))} /> ); diff --git a/docker-compose.yml b/docker-compose.yml index d8a81025..c995fd84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,9 +12,10 @@ services: # Node environment NODE_ENV: production - # REQUIRED: Change this to a random secure string in production + # REQUIRED: Session secret for encrypting cookies and sessions # Generate with: openssl rand -base64 32 - SESSION_SECRET: ${SESSION_SECRET:-change-me-in-production} + # SECURITY: You MUST set this to a unique value in production! + SESSION_SECRET: ${SESSION_SECRET:?ERROR - SESSION_SECRET is required} # Caddy API endpoint (internal communication) CADDY_API_URL: ${CADDY_API_URL:-http://caddy:2019} @@ -30,9 +31,10 @@ services: NEXTAUTH_URL: ${BASE_URL:-http://localhost:3000} # REQUIRED: Admin credentials for login - # WARNING: Change these values! Do not use defaults in production! - ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} - ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin} + # SECURITY: You MUST set these to secure values in production! + # Password must be 12+ chars with uppercase, lowercase, numbers, and special chars + ADMIN_USERNAME: ${ADMIN_USERNAME:?ERROR - ADMIN_USERNAME is required} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ERROR - ADMIN_PASSWORD is required} volumes: - ./data:/app/data depends_on: diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index c0ead8c0..f249921d 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -13,14 +13,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ && rm -rf /var/lib/apt/lists/* COPY package.json package-lock.json* ./ -# Copy prisma schema before npm ci so postinstall can generate -COPY prisma ./prisma -# Set temporary DATABASE_URL for Prisma CLI -ENV DATABASE_URL=file:/tmp/dev.db -# Install dependencies (postinstall will run prisma generate) +# Install dependencies RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi -# Explicitly verify Prisma client is generated -RUN npx prisma generate FROM base AS builder ENV NODE_ENV=production @@ -28,16 +22,8 @@ ENV NEXT_TELEMETRY_DISABLED=1 # Set a temporary database path for build ENV DATABASE_PATH=/tmp/build.db ENV DATABASE_URL=file:/tmp/build.db -# Install openssl for Prisma query engine -RUN apt-get update && apt-get install -y --no-install-recommends \ - openssl \ - && rm -rf /var/lib/apt/lists/* COPY --from=deps /app/node_modules ./node_modules COPY . . -# Generate Prisma client (ensures it's available in this stage) -RUN npx prisma generate -# Push schema to temporary database for build-time data access -RUN npx prisma db push # Build the Next.js application RUN npm run build && rm -f /tmp/build.db @@ -46,10 +32,9 @@ ENV NODE_ENV=production ENV PORT=3000 WORKDIR /app -# Install gosu for privilege dropping and openssl for Prisma +# Install gosu for privilege dropping RUN apt-get update && apt-get install -y --no-install-recommends \ gosu \ - openssl \ && rm -rf /var/lib/apt/lists/* RUN groupadd -g 1001 nodejs && useradd -r -u 1001 -g nodejs nextjs @@ -63,11 +48,8 @@ COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/.next/server/instrumentation.js ./.next/server/instrumentation.js COPY --from=builder /app/.next/server/instrumentation ./.next/server/instrumentation COPY --from=builder /app/.next/server/chunks/ ./.next/server/chunks/ - -# Copy Prisma client -COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma -COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma -COPY --from=builder /app/prisma ./prisma +# Copy Drizzle migrations for runtime schema management +COPY --from=builder /app/drizzle ./drizzle # Create data directory for SQLite database RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data diff --git a/docker/web/entrypoint.sh b/docker/web/entrypoint.sh old mode 100755 new mode 100644 index 2d2787db..12cf7ffd --- a/docker/web/entrypoint.sh +++ b/docker/web/entrypoint.sh @@ -1,50 +1,12 @@ #!/bin/sh set -e -# This script runs as root first to fix permissions, then switches to nextjs user - DB_PATH="${DATABASE_PATH:-/app/data/caddy-proxy-manager.db}" DB_DIR=$(dirname "$DB_PATH") -echo "Setting up database directory permissions..." - -# Ensure the data directory is owned by nextjs user +echo "Ensuring database directory exists..." +mkdir -p "$DB_DIR" chown -R nextjs:nodejs "$DB_DIR" -# Ensure node_modules is owned by nextjs user for Prisma client generation -chown -R nextjs:nodejs /app/node_modules - -# Remove old Prisma client to avoid permission issues during regeneration -echo "Cleaning old Prisma client..." -rm -rf /app/node_modules/.prisma/client - -# Switch to nextjs user and initialize database if needed -gosu nextjs sh -c ' - DB_PATH="'"$DB_PATH"'" - - # Set npm cache to writable directory - export NPM_CONFIG_CACHE=/tmp/.npm - - # Generate real Prisma client at runtime (replaces build-time stub) - echo "Generating Prisma client..." - npx prisma generate || { - echo "Warning: Prisma generate failed, attempting with checksum ignore..." - PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1 npx prisma generate || { - echo "Error: Failed to generate Prisma client" - exit 1 - } - } - - if [ ! -f "$DB_PATH" ]; then - echo "Database not found, initializing..." - npx prisma db push --skip-generate - echo "Database initialized successfully" - else - echo "Database exists, applying any schema changes..." - npx prisma db push --skip-generate --accept-data-loss 2>/dev/null || true - fi - - echo "Starting application..." - export HOSTNAME="0.0.0.0" - exec node server.js -' \ No newline at end of file +echo "Starting application..." +exec gosu nextjs env HOSTNAME=0.0.0.0 node server.js diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 00000000..d1404046 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + out: "./drizzle", + schema: "./src/lib/db/schema.ts", + dialect: "sqlite", + dbCredentials: { + url: process.env.DATABASE_URL ?? "file:./data/caddy-proxy-manager.db" + } +}); diff --git a/drizzle/0000_initial.sql b/drizzle/0000_initial.sql new file mode 100644 index 00000000..fafccc8f --- /dev/null +++ b/drizzle/0000_initial.sql @@ -0,0 +1,152 @@ +CREATE TABLE `access_list_entries` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `access_list_id` integer NOT NULL, + `username` text NOT NULL, + `password_hash` text NOT NULL, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + FOREIGN KEY (`access_list_id`) REFERENCES `access_lists`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `access_list_entries_list_idx` ON `access_list_entries` (`access_list_id`);--> statement-breakpoint +CREATE TABLE `access_lists` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `description` text, + `created_by` integer, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `api_tokens` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `token_hash` text NOT NULL, + `created_by` integer NOT NULL, + `created_at` text NOT NULL, + `last_used_at` text, + `expires_at` text, + FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `api_tokens_token_hash_unique` ON `api_tokens` (`token_hash`);--> statement-breakpoint +CREATE TABLE `audit_events` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer, + `action` text NOT NULL, + `entity_type` text NOT NULL, + `entity_id` integer, + `summary` text, + `data` text, + `created_at` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `certificates` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `type` text NOT NULL, + `domain_names` text NOT NULL, + `auto_renew` integer DEFAULT true NOT NULL, + `provider_options` text, + `certificate_pem` text, + `private_key_pem` text, + `created_by` integer, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `dead_hosts` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `domains` text NOT NULL, + `status_code` integer DEFAULT 503 NOT NULL, + `response_body` text, + `enabled` integer DEFAULT true NOT NULL, + `created_by` integer, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `oauth_states` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `state` text NOT NULL, + `code_verifier` text NOT NULL, + `redirect_to` text, + `created_at` text NOT NULL, + `expires_at` text NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `oauth_state_unique` ON `oauth_states` (`state`);--> statement-breakpoint +CREATE TABLE `proxy_hosts` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `domains` text NOT NULL, + `upstreams` text NOT NULL, + `certificate_id` integer, + `access_list_id` integer, + `owner_user_id` integer, + `ssl_forced` integer DEFAULT true NOT NULL, + `hsts_enabled` integer DEFAULT true NOT NULL, + `hsts_subdomains` integer DEFAULT false NOT NULL, + `allow_websocket` integer DEFAULT true NOT NULL, + `preserve_host_header` integer DEFAULT true NOT NULL, + `meta` text, + `enabled` integer DEFAULT true NOT NULL, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + `skip_https_hostname_validation` integer DEFAULT false NOT NULL, + FOREIGN KEY (`certificate_id`) REFERENCES `certificates`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`access_list_id`) REFERENCES `access_lists`(`id`) ON UPDATE no action ON DELETE set null, + FOREIGN KEY (`owner_user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `redirect_hosts` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text NOT NULL, + `domains` text NOT NULL, + `destination` text NOT NULL, + `status_code` integer DEFAULT 302 NOT NULL, + `preserve_query` integer DEFAULT true NOT NULL, + `enabled` integer DEFAULT true NOT NULL, + `created_by` integer, + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + FOREIGN KEY (`created_by`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE set null +); +--> statement-breakpoint +CREATE TABLE `sessions` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer NOT NULL, + `token` text NOT NULL, + `expires_at` text NOT NULL, + `created_at` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint +CREATE TABLE `settings` ( + `key` text PRIMARY KEY NOT NULL, + `value` text NOT NULL, + `updated_at` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `email` text NOT NULL, + `name` text, + `password_hash` text, + `role` text DEFAULT 'user' NOT NULL, + `provider` text NOT NULL, + `subject` text NOT NULL, + `avatar_url` text, + `status` text DEFAULT 'active' NOT NULL, + `created_at` text NOT NULL, + `updated_at` text NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);--> statement-breakpoint +CREATE INDEX `users_provider_subject_idx` ON `users` (`provider`,`subject`); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..4c675c72 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1046 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "b14592c5-88b2-43bd-910e-bd39d63b6923", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "access_list_entries": { + "name": "access_list_entries", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "access_list_id": { + "name": "access_list_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "access_list_entries_list_idx": { + "name": "access_list_entries_list_idx", + "columns": [ + "access_list_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "access_list_entries_access_list_id_access_lists_id_fk": { + "name": "access_list_entries_access_list_id_access_lists_id_fk", + "tableFrom": "access_list_entries", + "tableTo": "access_lists", + "columnsFrom": [ + "access_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "access_lists": { + "name": "access_lists", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "access_lists_created_by_users_id_fk": { + "name": "access_lists_created_by_users_id_fk", + "tableFrom": "access_lists", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "api_tokens": { + "name": "api_tokens", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "api_tokens_token_hash_unique": { + "name": "api_tokens_token_hash_unique", + "columns": [ + "token_hash" + ], + "isUnique": true + } + }, + "foreignKeys": { + "api_tokens_created_by_users_id_fk": { + "name": "api_tokens_created_by_users_id_fk", + "tableFrom": "api_tokens", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "audit_events_user_id_users_id_fk": { + "name": "audit_events_user_id_users_id_fk", + "tableFrom": "audit_events", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "certificates": { + "name": "certificates", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domain_names": { + "name": "domain_names", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auto_renew": { + "name": "auto_renew", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "provider_options": { + "name": "provider_options", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "certificate_pem": { + "name": "certificate_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "private_key_pem": { + "name": "private_key_pem", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "certificates_created_by_users_id_fk": { + "name": "certificates_created_by_users_id_fk", + "tableFrom": "certificates", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "dead_hosts": { + "name": "dead_hosts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domains": { + "name": "domains", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 503 + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "dead_hosts_created_by_users_id_fk": { + "name": "dead_hosts_created_by_users_id_fk", + "tableFrom": "dead_hosts", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "oauth_states": { + "name": "oauth_states", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "redirect_to": { + "name": "redirect_to", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "oauth_state_unique": { + "name": "oauth_state_unique", + "columns": [ + "state" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "proxy_hosts": { + "name": "proxy_hosts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domains": { + "name": "domains", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upstreams": { + "name": "upstreams", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "certificate_id": { + "name": "certificate_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_list_id": { + "name": "access_list_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ssl_forced": { + "name": "ssl_forced", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "hsts_enabled": { + "name": "hsts_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "hsts_subdomains": { + "name": "hsts_subdomains", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "allow_websocket": { + "name": "allow_websocket", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "preserve_host_header": { + "name": "preserve_host_header", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "skip_https_hostname_validation": { + "name": "skip_https_hostname_validation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "proxy_hosts_certificate_id_certificates_id_fk": { + "name": "proxy_hosts_certificate_id_certificates_id_fk", + "tableFrom": "proxy_hosts", + "tableTo": "certificates", + "columnsFrom": [ + "certificate_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "proxy_hosts_access_list_id_access_lists_id_fk": { + "name": "proxy_hosts_access_list_id_access_lists_id_fk", + "tableFrom": "proxy_hosts", + "tableTo": "access_lists", + "columnsFrom": [ + "access_list_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "proxy_hosts_owner_user_id_users_id_fk": { + "name": "proxy_hosts_owner_user_id_users_id_fk", + "tableFrom": "proxy_hosts", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "redirect_hosts": { + "name": "redirect_hosts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "domains": { + "name": "domains", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 302 + }, + "preserve_query": { + "name": "preserve_query", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "redirect_hosts_created_by_users_id_fk": { + "name": "redirect_hosts_created_by_users_id_fk", + "tableFrom": "redirect_hosts", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_provider_subject_idx": { + "name": "users_provider_subject_idx", + "columns": [ + "provider", + "subject" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 00000000..e1e1c47a --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "5", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1762515724134, + "tag": "0000_initial", + "breakpoints": true + } + ] +} diff --git a/package-lock.json b/package-lock.json index 684a0847..ff976091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,19 +7,17 @@ "": { "name": "caddy-proxy-manager", "version": "1.0.0", - "hasInstallScript": true, "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.4", - "@prisma/client": "^6.18.0", "@types/better-sqlite3": "^7.6.13", "bcryptjs": "^3.0.3", "better-sqlite3": "^12.4.1", + "drizzle-orm": "^0.44.7", "next": "^16.0.1", "next-auth": "^5.0.0-beta.30", - "prisma": "^6.18.0", "react": "^19.2.0", "react-dom": "^19.2.0" }, @@ -27,6 +25,7 @@ "@types/node": "^24.10.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", + "drizzle-kit": "^0.31.6", "eslint": "^9.39.1", "eslint-config-next": "^16.0.1", "typescript": "^5.9.3" @@ -91,6 +90,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -340,6 +340,13 @@ "node": ">=6.9.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.6.0.tgz", @@ -431,6 +438,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -474,6 +482,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -519,6 +528,884 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=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.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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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.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/@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.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/@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.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/@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/@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/@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/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -1217,6 +2104,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz", "integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.5", @@ -1645,85 +2533,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@prisma/client": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.18.0.tgz", - "integrity": "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "prisma": "*", - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "prisma": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/@prisma/config": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.18.0.tgz", - "integrity": "sha512-rgFzspCpwsE+q3OF/xkp0fI2SJ3PfNe9LLMmuSVbAZ4nN66WfBiKqJKo/hLz3ysxiPQZf8h1SMf2ilqPMeWATQ==", - "license": "Apache-2.0", - "dependencies": { - "c12": "3.1.0", - "deepmerge-ts": "7.1.5", - "effect": "3.18.4", - "empathic": "2.0.0" - } - }, - "node_modules/@prisma/debug": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz", - "integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==", - "license": "Apache-2.0" - }, - "node_modules/@prisma/engines": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz", - "integrity": "sha512-i5RzjGF/ex6AFgqEe2o1IW8iIxJGYVQJVRau13kHPYEL1Ck8Zvwuzamqed/1iIljs5C7L+Opiz5TzSsUebkriA==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.18.0", - "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", - "@prisma/fetch-engine": "6.18.0", - "@prisma/get-platform": "6.18.0" - } - }, - "node_modules/@prisma/engines-version": { - "version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f.tgz", - "integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==", - "license": "Apache-2.0" - }, - "node_modules/@prisma/fetch-engine": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz", - "integrity": "sha512-TdaBvTtBwP3IoqVYoGIYpD4mWlk0pJpjTJjir/xLeNWlwog7Sl3bD2J0jJ8+5+q/6RBg+acb9drsv5W6lqae7A==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.18.0", - "@prisma/engines-version": "6.18.0-8.34b5a692b7bd79939a9a2c3ef97d816e749cda2f", - "@prisma/get-platform": "6.18.0" - } - }, - "node_modules/@prisma/get-platform": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.18.0.tgz", - "integrity": "sha512-uXNJCJGhxTCXo2B25Ta91Rk1/Nmlqg9p7G9GKh8TPhxvAyXCvMNQoogj4JLEUy+3ku8g59cpyQIKFhqY2xO2bg==", - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "6.18.0" - } - }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1731,12 +2540,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "license": "MIT" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1762,6 +2565,7 @@ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -1813,6 +2617,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1882,6 +2687,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2412,6 +3218,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2759,6 +3566,7 @@ "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==", "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -2831,6 +3639,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2869,33 +3678,12 @@ "ieee754": "^1.1.13" } }, - "node_modules/c12": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", - "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.3", - "confbox": "^0.2.2", - "defu": "^6.1.4", - "dotenv": "^16.6.1", - "exsolve": "^1.0.7", - "giget": "^2.0.0", - "jiti": "^2.4.2", - "ohash": "^2.0.11", - "pathe": "^2.0.3", - "perfect-debounce": "^1.0.0", - "pkg-types": "^2.2.0", - "rc9": "^2.1.2" - }, - "peerDependencies": { - "magicast": "^0.3.5" - }, - "peerDependenciesMeta": { - "magicast": { - "optional": true - } - } + "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/call-bind": { "version": "1.0.8", @@ -2993,36 +3781,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, - "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3065,21 +3829,6 @@ "dev": true, "license": "MIT" }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -3232,15 +3981,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3277,18 +4017,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "license": "MIT" - }, - "node_modules/destr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", - "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", - "license": "MIT" - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3308,16 +4036,145 @@ "csstype": "^3.0.2" } }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" + "node_modules/drizzle-kit": { + "version": "0.31.6", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.6.tgz", + "integrity": "sha512-/B4e/4pwnx25QwD5xXgdpo1S+077a2VZdosXbItE/oNmUgQwZydGDz9qJYmnQl/b+5IX0rLfwRhrPnroGtrg8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "esbuild-register": "^3.5.0" }, - "funding": { - "url": "https://dotenvx.com" + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.44.7", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.7.tgz", + "integrity": "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==", + "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.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "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/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } } }, "node_modules/dunder-proto": { @@ -3335,16 +4192,6 @@ "node": ">= 0.4" } }, - "node_modules/effect": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", - "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "fast-check": "^3.23.1" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.244", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", @@ -3359,15 +4206,6 @@ "dev": true, "license": "MIT" }, - "node_modules/empathic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3563,6 +4401,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", + "peer": true, + "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/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/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3591,6 +4485,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3776,6 +4671,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -4090,34 +4986,6 @@ "node": ">=6" } }, - "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "license": "MIT" - }, - "node_modules/fast-check": { - "version": "3.23.2", - "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", - "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT", - "dependencies": { - "pure-rand": "^6.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4407,23 +5275,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/giget": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", - "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.0", - "defu": "^6.1.4", - "node-fetch-native": "^1.6.6", - "nypm": "^0.6.0", - "pathe": "^2.0.3" - }, - "bin": { - "giget": "dist/cli.mjs" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -5134,15 +5985,6 @@ "node": ">= 0.4" } }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/jose": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", @@ -5466,6 +6308,7 @@ "resolved": "https://registry.npmjs.org/next/-/next-16.0.1.tgz", "integrity": "sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==", "license": "MIT", + "peer": true, "dependencies": { "@next/env": "16.0.1", "@swc/helpers": "0.5.15", @@ -5552,12 +6395,6 @@ "node": ">=10" } }, - "node_modules/node-fetch-native": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", - "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -5565,25 +6402,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", - "license": "MIT", - "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", - "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" - }, - "bin": { - "nypm": "dist/cli.mjs" - }, - "engines": { - "node": "^14.16.0 || >=16.10.0" - } - }, "node_modules/oauth4webapi": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.2.tgz", @@ -5715,12 +6533,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", - "license": "MIT" - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5863,18 +6675,6 @@ "node": ">=8" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, - "node_modules/perfect-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", - "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5894,17 +6694,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5948,6 +6737,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -5998,31 +6788,6 @@ "node": ">= 0.8.0" } }, - "node_modules/prisma": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.18.0.tgz", - "integrity": "sha512-bXWy3vTk8mnRmT+SLyZBQoC2vtV9Z8u7OHvEu+aULYxwiop/CPiFZ+F56KsNRNf35jw+8wcu8pmLsjxpBxAO9g==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/config": "6.18.0", - "@prisma/engines": "6.18.0" - }, - "bin": { - "prisma": "build/index.js" - }, - "engines": { - "node": ">=18.18" - }, - "peerDependencies": { - "typescript": ">=5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6054,22 +6819,6 @@ "node": ">=6" } }, - "node_modules/pure-rand": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", - "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6115,21 +6864,12 @@ "node": ">=0.10.0" } }, - "node_modules/rc9": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", - "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", - "license": "MIT", - "dependencies": { - "defu": "^6.1.4", - "destr": "^2.0.3" - } - }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6139,6 +6879,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6182,19 +6923,6 @@ "node": ">= 6" } }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -6660,6 +7388,27 @@ "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/source-map-support/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/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -6908,12 +7657,6 @@ "node": ">=6" } }, - "node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -6955,6 +7698,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7114,8 +7858,9 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7411,6 +8156,7 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 4d81888b..522b9ac2 100644 --- a/package.json +++ b/package.json @@ -9,24 +9,25 @@ "start": "next start", "lint": "next lint", "typecheck": "tsc --noEmit", - "postinstall": "prisma generate" + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate" }, "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@mui/icons-material": "^7.3.5", "@mui/material": "^7.3.4", - "@prisma/client": "^6.18.0", "@types/better-sqlite3": "^7.6.13", "bcryptjs": "^3.0.3", "better-sqlite3": "^12.4.1", + "drizzle-orm": "^0.44.7", "next": "^16.0.1", "next-auth": "^5.0.0-beta.30", - "prisma": "^6.18.0", "react": "^19.2.0", "react-dom": "^19.2.0" }, "devDependencies": { + "drizzle-kit": "^0.31.6", "@types/node": "^24.10.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 64464a0c..00000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,206 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -generator client { - provider = "prisma-client-js" - binaryTargets = ["native", "debian-openssl-3.0.x"] -} - -datasource db { - provider = "sqlite" - url = env("DATABASE_URL") -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - name String? - passwordHash String? @map("password_hash") - role String @default("user") - provider String - subject String - avatarUrl String? @map("avatar_url") - status String @default("active") - createdAt DateTime @map("created_at") - updatedAt DateTime @map("updated_at") - - sessions Session[] - accessLists AccessList[] - certificates Certificate[] - proxyHosts ProxyHost[] - redirectHosts RedirectHost[] - deadHosts DeadHost[] - apiTokens ApiToken[] - auditEvents AuditEvent[] - - @@map("users") -} - -model Session { - id Int @id @default(autoincrement()) - userId Int @map("user_id") - token String @unique - expiresAt DateTime @map("expires_at") - createdAt DateTime @map("created_at") - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([token]) - @@map("sessions") -} - -model OAuthState { - id Int @id @default(autoincrement()) - state String @unique - codeVerifier String @map("code_verifier") - redirectTo String? @map("redirect_to") - createdAt DateTime @map("created_at") - expiresAt DateTime @map("expires_at") - - @@map("oauth_states") -} - -model Setting { - key String @id - value String - updatedAt DateTime @map("updated_at") - - @@map("settings") -} - -model AccessList { - id Int @id @default(autoincrement()) - name String - description String? - createdBy Int? @map("created_by") - createdAt DateTime @map("created_at") - updatedAt DateTime @map("updated_at") - - user User? @relation(fields: [createdBy], references: [id], onDelete: SetNull) - entries AccessListEntry[] - proxyHosts ProxyHost[] - - @@map("access_lists") -} - -model AccessListEntry { - id Int @id @default(autoincrement()) - accessListId Int @map("access_list_id") - username String - passwordHash String @map("password_hash") - createdAt DateTime @map("created_at") - updatedAt DateTime @map("updated_at") - - accessList AccessList @relation(fields: [accessListId], references: [id], onDelete: Cascade) - - @@index([accessListId]) - @@map("access_list_entries") -} - -model Certificate { - id Int @id @default(autoincrement()) - name String - type String - domainNames String @map("domain_names") - autoRenew Boolean @default(true) @map("auto_renew") - providerOptions String? @map("provider_options") - certificatePem String? @map("certificate_pem") - privateKeyPem String? @map("private_key_pem") - createdBy Int? @map("created_by") - createdAt DateTime @map("created_at") - updatedAt DateTime @map("updated_at") - - user User? @relation(fields: [createdBy], references: [id], onDelete: SetNull) - proxyHosts ProxyHost[] - - @@map("certificates") -} - -model ProxyHost { - id Int @id @default(autoincrement()) - name String - domains String - upstreams String - certificateId Int? @map("certificate_id") - accessListId Int? @map("access_list_id") - ownerUserId Int? @map("owner_user_id") - sslForced Boolean @default(true) @map("ssl_forced") - hstsEnabled Boolean @default(true) @map("hsts_enabled") - hstsSubdomains Boolean @default(false) @map("hsts_subdomains") - allowWebsocket Boolean @default(true) @map("allow_websocket") - preserveHostHeader Boolean @default(true) @map("preserve_host_header") - meta String? - enabled Boolean @default(true) - createdAt DateTime @map("created_at") - updatedAt DateTime @map("updated_at") - skipHttpsHostnameValidation Boolean @default(false) @map("skip_https_hostname_validation") - - certificate Certificate? @relation(fields: [certificateId], references: [id], onDelete: SetNull) - accessList AccessList? @relation(fields: [accessListId], references: [id], onDelete: SetNull) - owner User? @relation(fields: [ownerUserId], references: [id], onDelete: SetNull) - - @@map("proxy_hosts") -} - -model RedirectHost { - id Int @id @default(autoincrement()) - name String - domains String - destination String - statusCode Int @default(302) @map("status_code") - preserveQuery Boolean @default(true) @map("preserve_query") - enabled Boolean @default(true) - createdBy Int? @map("created_by") - createdAt DateTime @map("created_at") - updatedAt DateTime @map("updated_at") - - user User? @relation(fields: [createdBy], references: [id], onDelete: SetNull) - - @@map("redirect_hosts") -} - -model DeadHost { - id Int @id @default(autoincrement()) - name String - domains String - statusCode Int @default(503) @map("status_code") - responseBody String? @map("response_body") - enabled Boolean @default(true) - createdBy Int? @map("created_by") - createdAt DateTime @map("created_at") - updatedAt DateTime @map("updated_at") - - user User? @relation(fields: [createdBy], references: [id], onDelete: SetNull) - - @@map("dead_hosts") -} - -model ApiToken { - id Int @id @default(autoincrement()) - name String - tokenHash String @unique @map("token_hash") - createdBy Int @map("created_by") - createdAt DateTime @map("created_at") - lastUsedAt DateTime? @map("last_used_at") - expiresAt DateTime? @map("expires_at") - - user User @relation(fields: [createdBy], references: [id], onDelete: Cascade) - - @@index([tokenHash]) - @@map("api_tokens") -} - -model AuditEvent { - id Int @id @default(autoincrement()) - userId Int? @map("user_id") - action String - entityType String @map("entity_type") - entityId Int? @map("entity_id") - summary String? - data String? - createdAt DateTime @map("created_at") - - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - - @@map("audit_events") -} diff --git a/src/lib/audit.ts b/src/lib/audit.ts index f54705d8..ccffb95c 100644 --- a/src/lib/audit.ts +++ b/src/lib/audit.ts @@ -1,4 +1,5 @@ -import prisma, { nowIso } from "./db"; +import db, { nowIso } from "./db"; +import { auditEvents } from "./db/schema"; export function logAuditEvent(params: { userId?: number | null; @@ -8,18 +9,18 @@ export function logAuditEvent(params: { summary?: string | null; data?: unknown; }) { - prisma.auditEvent.create({ - data: { + try { + db.insert(auditEvents).values({ userId: params.userId ?? null, action: params.action, entityType: params.entityType, entityId: params.entityId ?? null, summary: params.summary ?? null, data: params.data ? JSON.stringify(params.data) : null, - createdAt: new Date(nowIso()) - } - }).catch((error: unknown) => { + createdAt: nowIso() + }).run(); + } catch (error) { // Log error but don't throw to avoid breaking the main flow console.error("Failed to log audit event:", error); - }); + } } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index c5f28ffa..6444655b 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,8 +1,9 @@ import NextAuth, { type DefaultSession } from "next-auth"; import Credentials from "next-auth/providers/credentials"; import bcrypt from "bcryptjs"; -import prisma from "./db"; +import db from "./db"; import { config } from "./config"; +import { users } from "./db/schema"; declare module "next-auth" { interface Session { @@ -36,8 +37,8 @@ function createCredentialsProvider() { // Look up user in database by email (constructed from username) const email = `${username}@localhost`; - const user = await prisma.user.findUnique({ - where: { email } + const user = await db.query.users.findFirst({ + where: (table, operators) => operators.eq(table.email, email) }); if (!user || user.status !== "active" || !user.passwordHash) { diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts index c0748dc3..4e69977d 100644 --- a/src/lib/caddy.ts +++ b/src/lib/caddy.ts @@ -1,9 +1,16 @@ import { chmodSync, mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import crypto from "node:crypto"; -import prisma, { nowIso } from "./db"; +import db, { nowIso } from "./db"; import { config } from "./config"; import { getCloudflareSettings, getGeneralSettings, setSetting } from "./settings"; +import { + accessListEntries, + certificates, + deadHosts, + proxyHosts, + redirectHosts +} from "./db/schema"; const CERTS_DIR = process.env.CERTS_DIRECTORY || join(process.cwd(), "data", "certs"); mkdirSync(CERTS_DIR, { recursive: true, mode: 0o700 }); @@ -721,69 +728,68 @@ async function buildTlsAutomation( } async function buildCaddyDocument() { - const [proxyHosts, redirectHosts, deadHosts, certRows, accessListEntries] = await Promise.all([ - prisma.proxyHost.findMany({ - select: { - id: true, - name: true, - domains: true, - upstreams: true, - certificateId: true, - accessListId: true, - sslForced: true, - hstsEnabled: true, - hstsSubdomains: true, - allowWebsocket: true, - preserveHostHeader: true, - skipHttpsHostnameValidation: true, - meta: true, - enabled: true - } - }), - prisma.redirectHost.findMany({ - select: { - id: true, - name: true, - domains: true, - destination: true, - statusCode: true, - preserveQuery: true, - enabled: true - } - }), - prisma.deadHost.findMany({ - select: { - id: true, - name: true, - domains: true, - statusCode: true, - responseBody: true, - enabled: true - } - }), - prisma.certificate.findMany({ - select: { - id: true, - name: true, - type: true, - domainNames: true, - certificatePem: true, - privateKeyPem: true, - autoRenew: true, - providerOptions: true - } - }), - prisma.accessListEntry.findMany({ - select: { - accessListId: true, - username: true, - passwordHash: true - } - }) + const [proxyHostRecords, redirectHostRecords, deadHostRecords, certRows, accessListEntryRecords] = await Promise.all([ + db + .select({ + id: proxyHosts.id, + name: proxyHosts.name, + domains: proxyHosts.domains, + upstreams: proxyHosts.upstreams, + certificateId: proxyHosts.certificateId, + accessListId: proxyHosts.accessListId, + sslForced: proxyHosts.sslForced, + hstsEnabled: proxyHosts.hstsEnabled, + hstsSubdomains: proxyHosts.hstsSubdomains, + allowWebsocket: proxyHosts.allowWebsocket, + preserveHostHeader: proxyHosts.preserveHostHeader, + skipHttpsHostnameValidation: proxyHosts.skipHttpsHostnameValidation, + meta: proxyHosts.meta, + enabled: proxyHosts.enabled + }) + .from(proxyHosts), + db + .select({ + id: redirectHosts.id, + name: redirectHosts.name, + domains: redirectHosts.domains, + destination: redirectHosts.destination, + statusCode: redirectHosts.statusCode, + preserveQuery: redirectHosts.preserveQuery, + enabled: redirectHosts.enabled + }) + .from(redirectHosts), + db + .select({ + id: deadHosts.id, + name: deadHosts.name, + domains: deadHosts.domains, + statusCode: deadHosts.statusCode, + responseBody: deadHosts.responseBody, + enabled: deadHosts.enabled + }) + .from(deadHosts), + db + .select({ + id: certificates.id, + name: certificates.name, + type: certificates.type, + domainNames: certificates.domainNames, + certificatePem: certificates.certificatePem, + privateKeyPem: certificates.privateKeyPem, + autoRenew: certificates.autoRenew, + providerOptions: certificates.providerOptions + }) + .from(certificates), + db + .select({ + accessListId: accessListEntries.accessListId, + username: accessListEntries.username, + passwordHash: accessListEntries.passwordHash + }) + .from(accessListEntries) ]); - // Map Prisma results to expected types - const proxyHostRows: ProxyHostRow[] = proxyHosts.map((h: typeof proxyHosts[0]) => ({ + const proxyHostRows: ProxyHostRow[] = proxyHostRecords.map((h) => ({ id: h.id, name: h.name, domains: h.domains, @@ -800,7 +806,7 @@ async function buildCaddyDocument() { enabled: h.enabled ? 1 : 0 })); - const redirectHostRows: RedirectHostRow[] = redirectHosts.map((h: typeof redirectHosts[0]) => ({ + const redirectHostRows: RedirectHostRow[] = redirectHostRecords.map((h) => ({ id: h.id, name: h.name, domains: h.domains, @@ -810,7 +816,7 @@ async function buildCaddyDocument() { enabled: h.enabled ? 1 : 0 })); - const deadHostRows: DeadHostRow[] = deadHosts.map((h: typeof deadHosts[0]) => ({ + const deadHostRows: DeadHostRow[] = deadHostRecords.map((h) => ({ id: h.id, name: h.name, domains: h.domains, @@ -830,10 +836,10 @@ async function buildCaddyDocument() { provider_options: c.providerOptions })); - const accessListEntryRows: AccessListEntryRow[] = accessListEntries.map((e: typeof accessListEntries[0]) => ({ - access_list_id: e.accessListId, - username: e.username, - password_hash: e.passwordHash + const accessListEntryRows: AccessListEntryRow[] = accessListEntryRecords.map((entry) => ({ + access_list_id: entry.accessListId, + username: entry.username, + password_hash: entry.passwordHash })); const certificateMap = new Map(certRowsMapped.map((cert) => [cert.id, cert])); diff --git a/src/lib/config.ts b/src/lib/config.ts index 2f045787..ac61daee 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,62 +1,127 @@ const DEV_SECRET = "dev-secret-change-in-production-12345678901234567890123456789012"; const DEFAULT_ADMIN_USERNAME = "admin"; const DEFAULT_ADMIN_PASSWORD = "admin"; -const DISALLOWED_SESSION_SECRETS = new Set([DEV_SECRET, "change-me-in-production"]); +const DISALLOWED_SESSION_SECRETS = new Set([ + "change-me-in-production", + "dev-secret-change-in-production-12345678901234567890123456789012" +]); const DEFAULT_CADDY_URL = process.env.NODE_ENV === "development" ? "http://localhost:2019" : "http://caddy:2019"; const MIN_SESSION_SECRET_LENGTH = 32; const MIN_ADMIN_PASSWORD_LENGTH = 12; const isProduction = process.env.NODE_ENV === "production"; const isNodeRuntime = process.env.NEXT_RUNTIME === "nodejs"; -const allowDevFallback = !isProduction || !isNodeRuntime; -const isRuntimeProduction = isProduction && isNodeRuntime; +const isDevelopment = process.env.NODE_ENV === "development"; +// Only enforce strict validation in actual production runtime, not during build +const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build" || !process.env.NEXT_RUNTIME; +const isRuntimeProduction = isProduction && isNodeRuntime && !isBuildPhase; function resolveSessionSecret(): string { const rawSecret = process.env.SESSION_SECRET ?? null; const secret = rawSecret?.trim(); - // Always return a value (build phase needs this) - if (!secret) { + // In development, allow missing secret + if (isDevelopment && !secret) { return DEV_SECRET; } - // Only validate in actual runtime production (not during build) + // In production build phase, allow temporary value + if (isProduction && !isNodeRuntime && !secret) { + return DEV_SECRET; + } + + // Use provided secret or dev secret + const finalSecret = secret || DEV_SECRET; + + // Strict validation in production runtime if (isRuntimeProduction) { + if (!secret) { + throw new Error( + "SESSION_SECRET environment variable is required in production. " + + "Generate a secure secret with: openssl rand -base64 32" + ); + } if (DISALLOWED_SESSION_SECRETS.has(secret)) { - throw new Error("SESSION_SECRET is using a known insecure placeholder value. Provide a unique secret."); + throw new Error( + "SESSION_SECRET is using a known insecure placeholder value. " + + "Generate a secure secret with: openssl rand -base64 32" + ); } if (secret.length < MIN_SESSION_SECRET_LENGTH) { - throw new Error(`SESSION_SECRET must be at least ${MIN_SESSION_SECRET_LENGTH} characters long in production.`); + throw new Error( + `SESSION_SECRET must be at least ${MIN_SESSION_SECRET_LENGTH} characters long in production. ` + + "Generate a secure secret with: openssl rand -base64 32" + ); } } - return secret; + return finalSecret; } function resolveAdminCredentials() { - const rawUsername = process.env.ADMIN_USERNAME ?? DEFAULT_ADMIN_USERNAME; - const rawPassword = process.env.ADMIN_PASSWORD ?? DEFAULT_ADMIN_PASSWORD; - const username = rawUsername?.trim(); - const password = rawPassword?.trim(); + const rawUsername = process.env.ADMIN_USERNAME ?? null; + const rawPassword = process.env.ADMIN_PASSWORD ?? null; + const username = rawUsername?.trim() || DEFAULT_ADMIN_USERNAME; + const password = rawPassword?.trim() || DEFAULT_ADMIN_PASSWORD; - // Always return values (build phase needs this) - if (!username || !password) { - return { username: DEFAULT_ADMIN_USERNAME, password: DEFAULT_ADMIN_PASSWORD }; + // In development, allow defaults + if (isDevelopment) { + if (username === DEFAULT_ADMIN_USERNAME || password === DEFAULT_ADMIN_PASSWORD) { + console.log("Using default admin credentials for development (admin/admin)"); + } + return { username, password }; } - // Only validate in actual runtime production (not during build) + // In production build phase, allow defaults temporarily + if (isProduction && !isNodeRuntime) { + return { username, password }; + } + + // Strict validation in production runtime if (isRuntimeProduction) { - if (username === DEFAULT_ADMIN_USERNAME) { - throw new Error("ADMIN_USERNAME must be changed from the default value when running in production."); + const errors: string[] = []; + + // Username validation - just ensure it's set + if (!rawUsername || !username) { + errors.push( + "ADMIN_USERNAME must be set" + ); } - if (password === DEFAULT_ADMIN_PASSWORD) { - throw new Error("ADMIN_PASSWORD must be changed from the default value when running in production."); + + // Password validation - strict requirements + if (!rawPassword || password === DEFAULT_ADMIN_PASSWORD) { + errors.push( + "ADMIN_PASSWORD must be set to a custom value in production (not 'admin')" + ); + } else { + if (password.length < MIN_ADMIN_PASSWORD_LENGTH) { + errors.push( + `ADMIN_PASSWORD must be at least ${MIN_ADMIN_PASSWORD_LENGTH} characters long` + ); + } + if (!/[A-Z]/.test(password) || !/[a-z]/.test(password)) { + errors.push( + "ADMIN_PASSWORD must include both uppercase and lowercase letters" + ); + } + if (!/[0-9]/.test(password)) { + errors.push( + "ADMIN_PASSWORD must include at least one number" + ); + } + if (!/[^A-Za-z0-9]/.test(password)) { + errors.push( + "ADMIN_PASSWORD must include at least one special character" + ); + } } - if (password.length < MIN_ADMIN_PASSWORD_LENGTH) { - throw new Error(`ADMIN_PASSWORD must be at least ${MIN_ADMIN_PASSWORD_LENGTH} characters long in production.`); - } - if (!/[A-Za-z]/.test(password) || !/[0-9]/.test(password)) { - throw new Error("ADMIN_PASSWORD must include both letters and numbers for adequate complexity."); + + if (errors.length > 0) { + throw new Error( + "Admin credentials validation failed:\n" + + errors.map(e => ` - ${e}`).join("\n") + + "\n\nSet secure credentials using ADMIN_USERNAME and ADMIN_PASSWORD environment variables." + ); } } diff --git a/src/lib/db.ts b/src/lib/db.ts index 5049a44c..7a7d6815 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,20 +1,128 @@ -import { PrismaClient } from "@prisma/client"; +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { mkdirSync } from "node:fs"; +import { dirname, isAbsolute, resolve as resolvePath } from "node:path"; +import * as schema from "./db/schema"; -// Prevent multiple instances of Prisma Client in development -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined; +const DEFAULT_SQLITE_URL = "file:./data/caddy-proxy-manager.db"; + +type GlobalForDrizzle = typeof globalThis & { + __DRIZZLE_DB__?: ReturnType>; + __SQLITE_CLIENT__?: Database.Database; + __MIGRATIONS_RAN__?: boolean; }; -export const prisma = - globalForPrisma.prisma ?? - new PrismaClient({ - log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"], - }); +function resolveSqlitePath(rawUrl: string): string { + if (!rawUrl) { + return ":memory:"; + } + if (rawUrl === ":memory:" || rawUrl === "file::memory:") { + return ":memory:"; + } -if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; + if (rawUrl.startsWith("file:./") || rawUrl.startsWith("file:../")) { + const relative = rawUrl.slice("file:".length); + return resolvePath(process.cwd(), relative); + } -export default prisma; + if (rawUrl.startsWith("file:")) { + try { + const fileUrl = new URL(rawUrl); + if (fileUrl.host && fileUrl.host !== "localhost") { + throw new Error("Remote SQLite hosts are not supported."); + } + return decodeURIComponent(fileUrl.pathname); + } catch { + const remainder = rawUrl.slice("file:".length); + if (!remainder) { + return ":memory:"; + } + return isAbsolute(remainder) ? remainder : resolvePath(process.cwd(), remainder); + } + } + + return isAbsolute(rawUrl) ? rawUrl : resolvePath(process.cwd(), rawUrl); +} + +const databaseUrl = process.env.DATABASE_URL ?? DEFAULT_SQLITE_URL; +const sqlitePath = resolveSqlitePath(databaseUrl); + +function ensureDirectoryFor(pathname: string) { + if (pathname === ":memory:") { + return; + } + const dir = dirname(pathname); + mkdirSync(dir, { recursive: true }); +} + +const globalForDrizzle = globalThis as GlobalForDrizzle; + +const sqlite = + globalForDrizzle.__SQLITE_CLIENT__ ?? + (() => { + ensureDirectoryFor(sqlitePath); + return new Database(sqlitePath); + })(); + +if (process.env.NODE_ENV !== "production") { + globalForDrizzle.__SQLITE_CLIENT__ = sqlite; +} + +export const db = + globalForDrizzle.__DRIZZLE_DB__ ?? drizzle(sqlite, { schema, casing: "snake_case" }); + +if (process.env.NODE_ENV !== "production") { + globalForDrizzle.__DRIZZLE_DB__ = db; +} + +const migrationsFolder = resolvePath(process.cwd(), "drizzle"); + +function runMigrations() { + if (sqlitePath === ":memory:") { + return; + } + if (globalForDrizzle.__MIGRATIONS_RAN__) { + return; + } + try { + migrate(db, { migrationsFolder }); + globalForDrizzle.__MIGRATIONS_RAN__ = true; + } catch (error: any) { + // During build, pages may be pre-rendered in parallel, causing race conditions + // with migrations. If tables already exist, just continue. + if (error?.code === 'SQLITE_ERROR' && error?.message?.includes('already exists')) { + console.log('Database tables already exist, skipping migrations'); + globalForDrizzle.__MIGRATIONS_RAN__ = true; + return; + } + throw error; + } +} + +try { + runMigrations(); +} catch (error) { + console.error("Failed to run database migrations:", error); + // In build mode, allow the build to continue even if migrations fail + // The runtime initialization will handle migrations properly + if (process.env.NODE_ENV !== 'production' || process.env.NEXT_PHASE === 'phase-production-build') { + console.warn('Continuing despite migration error during build phase'); + } else { + throw error; + } +} + +export { schema }; +export default db; export function nowIso(): string { return new Date().toISOString(); } + +export function toIso(value: string | Date | null | undefined): string | null { + if (!value) { + return null; + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); +} diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts new file mode 100644 index 00000000..271efbfe --- /dev/null +++ b/src/lib/db/schema.ts @@ -0,0 +1,175 @@ +import { integer, text, sqliteTable, uniqueIndex, index } from "drizzle-orm/sqlite-core"; + +export const users = sqliteTable( + "users", + { + id: integer("id").primaryKey({ autoIncrement: true }), + email: text("email").notNull(), + name: text("name"), + passwordHash: text("password_hash"), + role: text("role").notNull().default("user"), + provider: text("provider").notNull(), + subject: text("subject").notNull(), + avatarUrl: text("avatar_url"), + status: text("status").notNull().default("active"), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull() + }, + (table) => ({ + emailUnique: uniqueIndex("users_email_unique").on(table.email), + providerSubjectIdx: index("users_provider_subject_idx").on(table.provider, table.subject) + }) +); + +export const sessions = sqliteTable( + "sessions", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: integer("user_id") + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), + token: text("token").notNull(), + expiresAt: text("expires_at").notNull(), + createdAt: text("created_at").notNull() + }, + (table) => ({ + tokenUnique: uniqueIndex("sessions_token_unique").on(table.token) + }) +); + +export const oauthStates = sqliteTable( + "oauth_states", + { + id: integer("id").primaryKey({ autoIncrement: true }), + state: text("state").notNull(), + codeVerifier: text("code_verifier").notNull(), + redirectTo: text("redirect_to"), + createdAt: text("created_at").notNull(), + expiresAt: text("expires_at").notNull() + }, + (table) => ({ + stateUnique: uniqueIndex("oauth_state_unique").on(table.state) + }) +); + +export const settings = sqliteTable("settings", { + key: text("key").primaryKey(), + value: text("value").notNull(), + updatedAt: text("updated_at").notNull() +}); + +export const accessLists = sqliteTable("access_lists", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + description: text("description"), + createdBy: integer("created_by").references(() => users.id, { onDelete: "set null" }), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull() +}); + +export const accessListEntries = sqliteTable( + "access_list_entries", + { + id: integer("id").primaryKey({ autoIncrement: true }), + accessListId: integer("access_list_id") + .references(() => accessLists.id, { onDelete: "cascade" }) + .notNull(), + username: text("username").notNull(), + passwordHash: text("password_hash").notNull(), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull() + }, + (table) => ({ + accessListIdIdx: index("access_list_entries_list_idx").on(table.accessListId) + }) +); + +export const certificates = sqliteTable("certificates", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + type: text("type").notNull(), + domainNames: text("domain_names").notNull(), + autoRenew: integer("auto_renew", { mode: "boolean" }).notNull().default(true), + providerOptions: text("provider_options"), + certificatePem: text("certificate_pem"), + privateKeyPem: text("private_key_pem"), + createdBy: integer("created_by").references(() => users.id, { onDelete: "set null" }), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull() +}); + +export const proxyHosts = sqliteTable("proxy_hosts", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + domains: text("domains").notNull(), + upstreams: text("upstreams").notNull(), + certificateId: integer("certificate_id").references(() => certificates.id, { onDelete: "set null" }), + accessListId: integer("access_list_id").references(() => accessLists.id, { onDelete: "set null" }), + ownerUserId: integer("owner_user_id").references(() => users.id, { onDelete: "set null" }), + sslForced: integer("ssl_forced", { mode: "boolean" }).notNull().default(true), + hstsEnabled: integer("hsts_enabled", { mode: "boolean" }).notNull().default(true), + hstsSubdomains: integer("hsts_subdomains", { mode: "boolean" }).notNull().default(false), + allowWebsocket: integer("allow_websocket", { mode: "boolean" }).notNull().default(true), + preserveHostHeader: integer("preserve_host_header", { mode: "boolean" }).notNull().default(true), + meta: text("meta"), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull(), + skipHttpsHostnameValidation: integer("skip_https_hostname_validation", { mode: "boolean" }) + .notNull() + .default(false) +}); + +export const redirectHosts = sqliteTable("redirect_hosts", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + domains: text("domains").notNull(), + destination: text("destination").notNull(), + statusCode: integer("status_code").notNull().default(302), + preserveQuery: integer("preserve_query", { mode: "boolean" }).notNull().default(true), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + createdBy: integer("created_by").references(() => users.id, { onDelete: "set null" }), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull() +}); + +export const deadHosts = sqliteTable("dead_hosts", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + domains: text("domains").notNull(), + statusCode: integer("status_code").notNull().default(503), + responseBody: text("response_body"), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + createdBy: integer("created_by").references(() => users.id, { onDelete: "set null" }), + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull() +}); + +export const apiTokens = sqliteTable( + "api_tokens", + { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + tokenHash: text("token_hash").notNull(), + createdBy: integer("created_by") + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), + createdAt: text("created_at").notNull(), + lastUsedAt: text("last_used_at"), + expiresAt: text("expires_at") + }, + (table) => ({ + tokenHashUnique: uniqueIndex("api_tokens_token_hash_unique").on(table.tokenHash) + }) +); + +export const auditEvents = sqliteTable("audit_events", { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: integer("user_id").references(() => users.id, { onDelete: "set null" }), + action: text("action").notNull(), + entityType: text("entity_type").notNull(), + entityId: integer("entity_id"), + summary: text("summary"), + data: text("data"), + createdAt: text("created_at").notNull() +}); diff --git a/src/lib/init-db.ts b/src/lib/init-db.ts index 89f39928..33eb5f50 100644 --- a/src/lib/init-db.ts +++ b/src/lib/init-db.ts @@ -1,6 +1,8 @@ import bcrypt from "bcryptjs"; -import prisma, { nowIso } from "./db"; +import db, { nowIso } from "./db"; import { config } from "./config"; +import { users } from "./db/schema"; +import { eq } from "drizzle-orm"; /** * Ensures the admin user from environment variables exists in the database. @@ -17,43 +19,41 @@ export async function ensureAdminUser(): Promise { const passwordHash = bcrypt.hashSync(config.adminPassword, 12); // Check if admin user already exists - const existingUser = await prisma.user.findUnique({ - where: { id: adminId } + const existingUser = await db.query.users.findFirst({ + where: (table, { eq }) => eq(table.id, adminId) }); if (existingUser) { // Admin user exists, update credentials if needed // Always update password hash to handle password changes in env vars - const now = new Date(nowIso()); - await prisma.user.update({ - where: { id: adminId }, - data: { + const now = nowIso(); + await db + .update(users) + .set({ email: adminEmail, subject, passwordHash, updatedAt: now - } - }); + }) + .where(eq(users.id, adminId)); console.log(`Updated admin user: ${config.adminUsername}`); return; } // Create admin user with hashed password - const now = new Date(nowIso()); - await prisma.user.create({ - data: { - id: adminId, - email: adminEmail, - name: config.adminUsername, - passwordHash, // Store hashed password instead of plaintext - role: "admin", - provider, - subject, - avatarUrl: null, - status: "active", - createdAt: now, - updatedAt: now - } + const now = nowIso(); + await db.insert(users).values({ + id: adminId, + email: adminEmail, + name: config.adminUsername, + passwordHash, + role: "admin", + provider, + subject, + avatarUrl: null, + status: "active", + createdAt: now, + updatedAt: now }); console.log(`Created admin user: ${config.adminUsername}`); diff --git a/src/lib/models/access-lists.ts b/src/lib/models/access-lists.ts index 2cb8700d..42265258 100644 --- a/src/lib/models/access-lists.ts +++ b/src/lib/models/access-lists.ts @@ -1,7 +1,9 @@ import bcrypt from "bcryptjs"; -import prisma, { nowIso } from "../db"; -import { logAuditEvent } from "../audit"; +import db, { nowIso, toIso } from "../db"; import { applyCaddyConfig } from "../caddy"; +import { logAuditEvent } from "../audit"; +import { accessListEntries, accessLists } from "../db/schema"; +import { asc, eq, inArray } from "drizzle-orm"; export type AccessListEntry = { id: number; @@ -25,94 +27,101 @@ export type AccessListInput = { users?: { username: string; password: string }[]; }; -function toAccessList( - row: { - id: number; - name: string; - description: string | null; - createdAt: Date; - updatedAt: Date; - }, - entries: { - id: number; - username: string; - createdAt: Date; - updatedAt: Date; - }[] -): AccessList { +type AccessListRow = typeof accessLists.$inferSelect; +type AccessListEntryRow = typeof accessListEntries.$inferSelect; + +function buildEntry(row: AccessListEntryRow): AccessListEntry { + return { + id: row.id, + username: row.username, + created_at: toIso(row.createdAt)!, + updated_at: toIso(row.updatedAt)! + }; +} + +function toAccessList(row: AccessListRow, entries: AccessListEntryRow[]): AccessList { return { id: row.id, name: row.name, description: row.description, - entries: entries.map((entry) => ({ - id: entry.id, - username: entry.username, - created_at: entry.createdAt.toISOString(), - updated_at: entry.updatedAt.toISOString() - })), - created_at: row.createdAt.toISOString(), - updated_at: row.updatedAt.toISOString() + entries: entries + .slice() + .sort((a, b) => a.username.localeCompare(b.username)) + .map(buildEntry), + created_at: toIso(row.createdAt)!, + updated_at: toIso(row.updatedAt)! }; } export async function listAccessLists(): Promise { - const lists = await prisma.accessList.findMany({ - orderBy: { name: "asc" }, - include: { - entries: { - select: { - id: true, - username: true, - createdAt: true, - updatedAt: true - }, - orderBy: { username: "asc" } - } - } + const lists = await db.query.accessLists.findMany({ + orderBy: (table) => asc(table.name) }); - return lists.map((list: typeof lists[0]) => toAccessList(list, list.entries)); + + if (lists.length === 0) { + return []; + } + + const listIds = lists.map((list) => list.id); + const entries = await db + .select() + .from(accessListEntries) + .where(inArray(accessListEntries.accessListId, listIds)); + + const entriesByList = new Map(); + for (const entry of entries) { + const bucket = entriesByList.get(entry.accessListId) ?? []; + bucket.push(entry); + entriesByList.set(entry.accessListId, bucket); + } + + return lists.map((list) => toAccessList(list, entriesByList.get(list.id) ?? [])); } export async function getAccessList(id: number): Promise { - const list = await prisma.accessList.findUnique({ - where: { id }, - include: { - entries: { - select: { - id: true, - username: true, - createdAt: true, - updatedAt: true - }, - orderBy: { username: "asc" } - } - } + const list = await db.query.accessLists.findFirst({ + where: (table, operators) => operators.eq(table.id, id) }); - return list ? toAccessList(list, list.entries) : null; + if (!list) { + return null; + } + const entries = await db + .select() + .from(accessListEntries) + .where(eq(accessListEntries.accessListId, id)) + .orderBy(asc(accessListEntries.username)); + return toAccessList(list, entries); } export async function createAccessList(input: AccessListInput, actorUserId: number) { - const now = new Date(nowIso()); + const now = nowIso(); - const accessList = await prisma.accessList.create({ - data: { + const [accessList] = await db + .insert(accessLists) + .values({ name: input.name.trim(), description: input.description ?? null, createdBy: actorUserId, createdAt: now, - updatedAt: now, - entries: input.users - ? { - create: input.users.map((account) => ({ - username: account.username, - passwordHash: bcrypt.hashSync(account.password, 10), - createdAt: now, - updatedAt: now - })) - } - : undefined - } - }); + updatedAt: now + }) + .returning(); + + if (!accessList) { + throw new Error("Failed to create access list"); + } + + if (input.users && input.users.length > 0) { + await db.insert(accessListEntries).values( + input.users.map((account) => ({ + accessListId: accessList.id, + username: account.username, + passwordHash: bcrypt.hashSync(account.password, 10), + createdAt: now, + updatedAt: now + })) + ); + } logAuditEvent({ userId: actorUserId, @@ -136,15 +145,15 @@ export async function updateAccessList( throw new Error("Access list not found"); } - const now = new Date(nowIso()); - await prisma.accessList.update({ - where: { id }, - data: { + const now = nowIso(); + await db + .update(accessLists) + .set({ name: input.name ?? existing.name, description: input.description ?? existing.description, updatedAt: now - } - }); + }) + .where(eq(accessLists.id, id)); logAuditEvent({ userId: actorUserId, @@ -163,23 +172,23 @@ export async function addAccessListEntry( entry: { username: string; password: string }, actorUserId: number ) { - const list = await prisma.accessList.findUnique({ - where: { id: accessListId } + const list = await db.query.accessLists.findFirst({ + where: (table, operators) => operators.eq(table.id, accessListId) }); if (!list) { throw new Error("Access list not found"); } - const now = new Date(nowIso()); + + const now = nowIso(); const hash = bcrypt.hashSync(entry.password, 10); - await prisma.accessListEntry.create({ - data: { - accessListId, - username: entry.username, - passwordHash: hash, - createdAt: now, - updatedAt: now - } + await db.insert(accessListEntries).values({ + accessListId, + username: entry.username, + passwordHash: hash, + createdAt: now, + updatedAt: now }); + logAuditEvent({ userId: actorUserId, action: "create", @@ -192,15 +201,15 @@ export async function addAccessListEntry( } export async function removeAccessListEntry(accessListId: number, entryId: number, actorUserId: number) { - const list = await prisma.accessList.findUnique({ - where: { id: accessListId } + const list = await db.query.accessLists.findFirst({ + where: (table, operators) => operators.eq(table.id, accessListId) }); if (!list) { throw new Error("Access list not found"); } - await prisma.accessListEntry.delete({ - where: { id: entryId } - }); + + await db.delete(accessListEntries).where(eq(accessListEntries.id, entryId)); + logAuditEvent({ userId: actorUserId, action: "delete", @@ -213,15 +222,15 @@ export async function removeAccessListEntry(accessListId: number, entryId: numbe } export async function deleteAccessList(id: number, actorUserId: number) { - const existing = await prisma.accessList.findUnique({ - where: { id } + const existing = await db.query.accessLists.findFirst({ + where: (table, operators) => operators.eq(table.id, id) }); if (!existing) { throw new Error("Access list not found"); } - await prisma.accessList.delete({ - where: { id } - }); + + await db.delete(accessLists).where(eq(accessLists.id, id)); + logAuditEvent({ userId: actorUserId, action: "delete", diff --git a/src/lib/models/audit.ts b/src/lib/models/audit.ts index a4ffe0fd..852793ee 100644 --- a/src/lib/models/audit.ts +++ b/src/lib/models/audit.ts @@ -1,4 +1,6 @@ -import prisma from "../db"; +import db, { toIso } from "../db"; +import { auditEvents } from "../db/schema"; +import { desc } from "drizzle-orm"; export type AuditEvent = { id: number; @@ -11,18 +13,19 @@ export type AuditEvent = { }; export async function listAuditEvents(limit = 100): Promise { - const events = await prisma.auditEvent.findMany({ - orderBy: { createdAt: "desc" }, - take: limit - }); + const events = await db + .select() + .from(auditEvents) + .orderBy(desc(auditEvents.createdAt)) + .limit(limit); - return events.map((event: typeof events[0]) => ({ + return events.map((event) => ({ id: event.id, user_id: event.userId, action: event.action, entity_type: event.entityType, entity_id: event.entityId, summary: event.summary, - created_at: event.createdAt.toISOString() + created_at: toIso(event.createdAt)! })); } diff --git a/src/lib/models/certificates.ts b/src/lib/models/certificates.ts index ed5987f0..a4547467 100644 --- a/src/lib/models/certificates.ts +++ b/src/lib/models/certificates.ts @@ -1,6 +1,8 @@ -import prisma, { nowIso } from "../db"; +import db, { nowIso, toIso } from "../db"; import { logAuditEvent } from "../audit"; import { applyCaddyConfig } from "../caddy"; +import { certificates } from "../db/schema"; +import { desc, eq } from "drizzle-orm"; export type CertificateType = "managed" | "imported"; @@ -27,18 +29,9 @@ export type CertificateInput = { private_key_pem?: string | null; }; -function parseCertificate(row: { - id: number; - name: string; - type: string; - domainNames: string; - autoRenew: boolean; - providerOptions: string | null; - certificatePem: string | null; - privateKeyPem: string | null; - createdAt: Date; - updatedAt: Date; -}): Certificate { +type CertificateRow = typeof certificates.$inferSelect; + +function parseCertificate(row: CertificateRow): Certificate { return { id: row.id, name: row.name, @@ -48,21 +41,19 @@ function parseCertificate(row: { provider_options: row.providerOptions ? JSON.parse(row.providerOptions) : null, certificate_pem: row.certificatePem, private_key_pem: row.privateKeyPem, - created_at: row.createdAt.toISOString(), - updated_at: row.updatedAt.toISOString() + created_at: toIso(row.createdAt)!, + updated_at: toIso(row.updatedAt)! }; } export async function listCertificates(): Promise { - const certificates = await prisma.certificate.findMany({ - orderBy: { createdAt: "desc" } - }); - return certificates.map(parseCertificate); + const rows = await db.select().from(certificates).orderBy(desc(certificates.createdAt)); + return rows.map(parseCertificate); } export async function getCertificate(id: number): Promise { - const cert = await prisma.certificate.findUnique({ - where: { id } + const cert = await db.query.certificates.findFirst({ + where: (table, { eq }) => eq(table.id, id) }); return cert ? parseCertificate(cert) : null; } @@ -80,9 +71,10 @@ function validateCertificateInput(input: CertificateInput) { export async function createCertificate(input: CertificateInput, actorUserId: number) { validateCertificateInput(input); - const now = new Date(nowIso()); - const record = await prisma.certificate.create({ - data: { + const now = nowIso(); + const [record] = await db + .insert(certificates) + .values({ name: input.name.trim(), type: input.type, domainNames: JSON.stringify( @@ -95,8 +87,13 @@ export async function createCertificate(input: CertificateInput, actorUserId: nu createdAt: now, updatedAt: now, createdBy: actorUserId - } - }); + }) + .returning(); + + if (!record) { + throw new Error("Failed to create certificate"); + } + logAuditEvent({ userId: actorUserId, action: "create", @@ -126,10 +123,10 @@ export async function updateCertificate(id: number, input: Partial { - const hosts = await prisma.deadHost.findMany({ - orderBy: { createdAt: "desc" } - }); + const hosts = await db.select().from(deadHosts).orderBy(desc(deadHosts.createdAt)); return hosts.map(parse); } export async function getDeadHost(id: number): Promise { - const host = await prisma.deadHost.findUnique({ - where: { id } + const host = await db.query.deadHosts.findFirst({ + where: (table, { eq }) => eq(table.id, id) }); return host ? parse(host) : null; } @@ -62,9 +55,10 @@ export async function createDeadHost(input: DeadHostInput, actorUserId: number) throw new Error("At least one domain is required"); } - const now = new Date(nowIso()); - const record = await prisma.deadHost.create({ - data: { + const now = nowIso(); + const [record] = await db + .insert(deadHosts) + .values({ name: input.name.trim(), domains: JSON.stringify(Array.from(new Set(input.domains.map((d) => d.trim().toLowerCase())))), statusCode: input.status_code ?? 503, @@ -73,8 +67,12 @@ export async function createDeadHost(input: DeadHostInput, actorUserId: number) createdAt: now, updatedAt: now, createdBy: actorUserId - } - }); + }) + .returning(); + + if (!record) { + throw new Error("Failed to create dead host"); + } logAuditEvent({ userId: actorUserId, action: "create", @@ -91,18 +89,18 @@ export async function updateDeadHost(id: number, input: Partial, if (!existing) { throw new Error("Dead host not found"); } - const now = new Date(nowIso()); - await prisma.deadHost.update({ - where: { id }, - data: { + const now = nowIso(); + await db + .update(deadHosts) + .set({ name: input.name ?? existing.name, domains: JSON.stringify(input.domains ? Array.from(new Set(input.domains)) : existing.domains), statusCode: input.status_code ?? existing.status_code, responseBody: input.response_body ?? existing.response_body, enabled: input.enabled ?? existing.enabled, updatedAt: now - } - }); + }) + .where(eq(deadHosts.id, id)); logAuditEvent({ userId: actorUserId, action: "update", @@ -119,9 +117,7 @@ export async function deleteDeadHost(id: number, actorUserId: number) { if (!existing) { throw new Error("Dead host not found"); } - await prisma.deadHost.delete({ - where: { id } - }); + await db.delete(deadHosts).where(eq(deadHosts.id, id)); logAuditEvent({ userId: actorUserId, action: "delete", diff --git a/src/lib/models/proxy-hosts.ts b/src/lib/models/proxy-hosts.ts index 08f7cd13..c5c9081a 100644 --- a/src/lib/models/proxy-hosts.ts +++ b/src/lib/models/proxy-hosts.ts @@ -1,6 +1,8 @@ -import prisma, { nowIso } from "../db"; +import db, { nowIso, toIso } from "../db"; import { applyCaddyConfig } from "../caddy"; import { logAuditEvent } from "../audit"; +import { proxyHosts } from "../db/schema"; +import { desc, eq } from "drizzle-orm"; const DEFAULT_AUTHENTIK_HEADERS = [ "X-Authentik-Username", @@ -94,25 +96,7 @@ export type ProxyHostInput = { authentik?: ProxyHostAuthentikInput | null; }; -type ProxyHostRow = { - id: number; - name: string; - domains: string; - upstreams: string; - certificateId: number | null; - accessListId: number | null; - ownerUserId: number | null; - sslForced: boolean; - hstsEnabled: boolean; - hstsSubdomains: boolean; - allowWebsocket: boolean; - preserveHostHeader: boolean; - meta: string | null; - skipHttpsHostnameValidation: boolean; - enabled: boolean; - createdAt: Date; - updatedAt: Date; -}; +type ProxyHostRow = typeof proxyHosts.$inferSelect; function normalizeMetaValue(value: string | null | undefined) { if (!value) { @@ -394,8 +378,8 @@ function parseProxyHost(row: ProxyHostRow): ProxyHost { preserve_host_header: row.preserveHostHeader, skip_https_hostname_validation: row.skipHttpsHostnameValidation, enabled: row.enabled, - created_at: row.createdAt.toISOString(), - updated_at: row.updatedAt.toISOString(), + created_at: toIso(row.createdAt)!, + updated_at: toIso(row.updatedAt)!, custom_reverse_proxy_json: meta.custom_reverse_proxy_json ?? null, custom_pre_handlers_json: meta.custom_pre_handlers_json ?? null, authentik: hydrateAuthentik(meta.authentik) @@ -403,9 +387,7 @@ function parseProxyHost(row: ProxyHostRow): ProxyHost { } export async function listProxyHosts(): Promise { - const hosts = await prisma.proxyHost.findMany({ - orderBy: { createdAt: "desc" } - }); + const hosts = await db.select().from(proxyHosts).orderBy(desc(proxyHosts.createdAt)); return hosts.map(parseProxyHost); } @@ -417,10 +399,11 @@ export async function createProxyHost(input: ProxyHostInput, actorUserId: number throw new Error("At least one upstream must be specified"); } - const now = new Date(nowIso()); + const now = nowIso(); const meta = buildMeta({}, input); - const record = await prisma.proxyHost.create({ - data: { + const [record] = await db + .insert(proxyHosts) + .values({ name: input.name.trim(), domains: JSON.stringify(Array.from(new Set(input.domains.map((d) => d.trim().toLowerCase())))), upstreams: JSON.stringify(Array.from(new Set(input.upstreams.map((u) => u.trim())))), @@ -437,8 +420,12 @@ export async function createProxyHost(input: ProxyHostInput, actorUserId: number enabled: input.enabled ?? true, createdAt: now, updatedAt: now - } - }); + }) + .returning(); + + if (!record) { + throw new Error("Failed to create proxy host"); + } logAuditEvent({ userId: actorUserId, @@ -454,8 +441,8 @@ export async function createProxyHost(input: ProxyHostInput, actorUserId: number } export async function getProxyHost(id: number): Promise { - const host = await prisma.proxyHost.findUnique({ - where: { id } + const host = await db.query.proxyHosts.findFirst({ + where: (table, { eq }) => eq(table.id, id) }); return host ? parseProxyHost(host) : null; } @@ -475,10 +462,10 @@ export async function updateProxyHost(id: number, input: Partial }; const meta = buildMeta(existingMeta, input); - const now = new Date(nowIso()); - await prisma.proxyHost.update({ - where: { id }, - data: { + const now = nowIso(); + await db + .update(proxyHosts) + .set({ name: input.name ?? existing.name, domains, upstreams, @@ -493,8 +480,8 @@ export async function updateProxyHost(id: number, input: Partial skipHttpsHostnameValidation: input.skip_https_hostname_validation ?? existing.skip_https_hostname_validation, enabled: input.enabled ?? existing.enabled, updatedAt: now - } - }); + }) + .where(eq(proxyHosts.id, id)); logAuditEvent({ userId: actorUserId, @@ -515,9 +502,7 @@ export async function deleteProxyHost(id: number, actorUserId: number) { throw new Error("Proxy host not found"); } - await prisma.proxyHost.delete({ - where: { id } - }); + await db.delete(proxyHosts).where(eq(proxyHosts.id, id)); logAuditEvent({ userId: actorUserId, action: "delete", diff --git a/src/lib/models/redirect-hosts.ts b/src/lib/models/redirect-hosts.ts index daede127..aac97d94 100644 --- a/src/lib/models/redirect-hosts.ts +++ b/src/lib/models/redirect-hosts.ts @@ -1,6 +1,8 @@ -import prisma, { nowIso } from "../db"; +import db, { nowIso, toIso } from "../db"; import { logAuditEvent } from "../audit"; import { applyCaddyConfig } from "../caddy"; +import { redirectHosts } from "../db/schema"; +import { desc, eq } from "drizzle-orm"; export type RedirectHost = { id: number; @@ -23,17 +25,9 @@ export type RedirectHostInput = { enabled?: boolean; }; -function parseDbRecord(record: { - id: number; - name: string; - domains: string; - destination: string; - statusCode: number; - preserveQuery: boolean; - enabled: boolean; - createdAt: Date; - updatedAt: Date; -}): RedirectHost { +type RedirectHostRow = typeof redirectHosts.$inferSelect; + +function parseDbRecord(record: RedirectHostRow): RedirectHost { return { id: record.id, name: record.name, @@ -42,21 +36,19 @@ function parseDbRecord(record: { status_code: record.statusCode, preserve_query: record.preserveQuery, enabled: record.enabled, - created_at: record.createdAt.toISOString(), - updated_at: record.updatedAt.toISOString() + created_at: toIso(record.createdAt)!, + updated_at: toIso(record.updatedAt)! }; } export async function listRedirectHosts(): Promise { - const records = await prisma.redirectHost.findMany({ - orderBy: { createdAt: "desc" } - }); + const records = await db.select().from(redirectHosts).orderBy(desc(redirectHosts.createdAt)); return records.map(parseDbRecord); } export async function getRedirectHost(id: number): Promise { - const record = await prisma.redirectHost.findUnique({ - where: { id } + const record = await db.query.redirectHosts.findFirst({ + where: (table, { eq }) => eq(table.id, id) }); return record ? parseDbRecord(record) : null; } @@ -66,9 +58,10 @@ export async function createRedirectHost(input: RedirectHostInput, actorUserId: throw new Error("At least one domain is required"); } - const now = new Date(nowIso()); - const record = await prisma.redirectHost.create({ - data: { + const now = nowIso(); + const [record] = await db + .insert(redirectHosts) + .values({ name: input.name.trim(), domains: JSON.stringify(Array.from(new Set(input.domains.map((d) => d.trim().toLowerCase())))), destination: input.destination.trim(), @@ -78,8 +71,12 @@ export async function createRedirectHost(input: RedirectHostInput, actorUserId: createdAt: now, updatedAt: now, createdBy: actorUserId - } - }); + }) + .returning(); + + if (!record) { + throw new Error("Failed to create redirect host"); + } logAuditEvent({ userId: actorUserId, @@ -98,10 +95,10 @@ export async function updateRedirectHost(id: number, input: Partial { - const user = await prisma.user.findUnique({ - where: { id: userId } + const user = await db.query.users.findFirst({ + where: (table, { eq }) => eq(table.id, userId) }); return user ? parseDbUser(user) : null; } export async function getUserCount(): Promise { - return await prisma.user.count(); + const result = await db.select({ value: count() }).from(users); + return result[0]?.value ?? 0; } export async function findUserByProviderSubject(provider: string, subject: string): Promise { - const user = await prisma.user.findFirst({ - where: { - provider, - subject - } + const user = await db.query.users.findFirst({ + where: (table, operators) => and(operators.eq(table.provider, provider), operators.eq(table.subject, subject)) }); return user ? parseDbUser(user) : null; } export async function findUserByEmail(email: string): Promise { const normalizedEmail = email.trim().toLowerCase(); - const user = await prisma.user.findFirst({ - where: { - email: normalizedEmail - } + const user = await db.query.users.findFirst({ + where: (table, { eq }) => eq(table.email, normalizedEmail) }); return user ? parseDbUser(user) : null; } @@ -82,12 +70,13 @@ export async function createUser(data: { avatar_url?: string | null; passwordHash?: string | null; }): Promise { - const now = new Date(nowIso()); + const now = nowIso(); const role = data.role ?? "user"; const email = data.email.trim().toLowerCase(); - const user = await prisma.user.create({ - data: { + const [user] = await db + .insert(users) + .values({ email, name: data.name ?? null, passwordHash: data.passwordHash ?? null, @@ -98,8 +87,8 @@ export async function createUser(data: { status: "active", createdAt: now, updatedAt: now - } - }); + }) + .returning(); return parseDbUser(user); } @@ -110,45 +99,46 @@ export async function updateUserProfile(userId: number, data: { email?: string; return null; } - const now = new Date(nowIso()); - const user = await prisma.user.update({ - where: { id: userId }, - data: { + const now = nowIso(); + const [updated] = await db + .update(users) + .set({ email: data.email ?? current.email, name: data.name ?? current.name, avatarUrl: data.avatar_url ?? current.avatar_url, updatedAt: now - } - }); + }) + .where(eq(users.id, userId)) + .returning(); - return parseDbUser(user); + return updated ? parseDbUser(updated) : null; } export async function updateUserPassword(userId: number, passwordHash: string): Promise { - const now = new Date(nowIso()); - await prisma.user.update({ - where: { id: userId }, - data: { + const now = nowIso(); + await db + .update(users) + .set({ passwordHash, updatedAt: now - } - }); + }) + .where(eq(users.id, userId)); } export async function listUsers(): Promise { - const users = await prisma.user.findMany({ - orderBy: { createdAt: "asc" } + const rows = await db.query.users.findMany({ + orderBy: (table, { asc }) => asc(table.createdAt) }); - return users.map(parseDbUser); + return rows.map(parseDbUser); } export async function promoteToAdmin(userId: number): Promise { - const now = new Date(nowIso()); - await prisma.user.update({ - where: { id: userId }, - data: { + const now = nowIso(); + await db + .update(users) + .set({ role: "admin", updatedAt: now - } - }); + }) + .where(eq(users.id, userId)); } diff --git a/src/lib/settings.ts b/src/lib/settings.ts index c0f0596e..3ee4fb3e 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -1,4 +1,6 @@ -import prisma, { nowIso } from "./db"; +import db, { nowIso } from "./db"; +import { settings } from "./db/schema"; +import { eq } from "drizzle-orm"; export type SettingValue = T | null; @@ -14,8 +16,8 @@ export type GeneralSettings = { }; export async function getSetting(key: string): Promise> { - const setting = await prisma.setting.findUnique({ - where: { key } + const setting = await db.query.settings.findFirst({ + where: (table, { eq }) => eq(table.key, key) }); if (!setting) { @@ -32,20 +34,22 @@ export async function getSetting(key: string): Promise> { export async function setSetting(key: string, value: T): Promise { const payload = JSON.stringify(value); - const now = new Date(nowIso()); + const now = nowIso(); - await prisma.setting.upsert({ - where: { key }, - update: { - value: payload, - updatedAt: now - }, - create: { + await db + .insert(settings) + .values({ key, value: payload, updatedAt: now - } - }); + }) + .onConflictDoUpdate({ + target: settings.key, + set: { + value: payload, + updatedAt: now + } + }); } export async function getCloudflareSettings(): Promise {