diff --git a/AGENTS.md b/AGENTS.md index 3dd4fea..05ab1b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,10 +43,14 @@ The application uses Prisma ORM with PostgreSQL. The schema defines two main mod - Date picker for filtering books by completion date - Default date range is current year (Jan 1 - Dec 31) -- **pages/admin.tsx**: Admin page for adding books - - Contains `upsertBooksAndAuthors` function for inserting/updating books - - Handles many-to-many author relationships - - **Note**: This page runs on client-side and directly calls Prisma, which will fail in production since Prisma is backend-only +- **pages/p/[id]/index.tsx**: Admin dashboard (protected) + - Full CRUD operations for books + - Uses dynamic route with server-side validation + - Only accessible at `/p/{ADMIN_PATH}` where path matches env var + +- **pages/p/[id]/login.tsx**: Admin login page + - Password-based authentication + - Sets HTTP-only cookie on successful login ### Components @@ -83,6 +87,9 @@ Add new domains to `next.config.mjs` remotePatterns if needed. Required in `.env`: - `DATABASE_URL`: PostgreSQL connection string for Prisma +- `ADMIN_PATH`: Secret URL path segment for admin access (required, no default) +- `ADMIN_PASSWORD`: Password for admin authentication (required) +- `AUTH_SECRET`: HMAC-SHA256 signing secret for authentication tokens (required, min 32 chars recommended; generate with `openssl rand -hex 32`) ## Key Implementation Patterns @@ -103,10 +110,46 @@ The date picker implementation: - Closes after both dates selected or picker cleared - Shows all books when date range is not fully set -### Book/Author Upsert Logic +### Admin Security (Path Obfuscation) + +The admin interface uses a defense-in-depth security approach with multiple layers: + +#### Security Layers + +1. **Secret URL Path**: The admin is only accessible at `/p/{ADMIN_PATH}` where `ADMIN_PATH` is an environment variable. There are no hardcoded paths like `/admin` in the codebase. + +2. **Middleware Enforcement** (`middleware.ts`): Intercepts all `/p/*` and `/api/p/*` requests, validates the path segment against `ADMIN_PATH`, and checks authentication tokens before allowing access. + +3. **Server-Side Validation** (`getServerSideProps`): Pages at `pages/p/[id]/` re-validate the path parameter, providing defense-in-depth even if middleware is bypassed. + +4. **API Route Validation**: API endpoints at `pages/api/p/[id]/` and protected routes in `pages/api/books/` validate authentication before processing write operations. + +#### Token-Based Authentication + +- **HMAC-SHA256 Signed Tokens**: Auth tokens are signed using `AUTH_SECRET` (see `lib/auth.ts`). Token format: `{timestamp}.{signature}`. +- **24-Hour Expiry**: Tokens expire after 24 hours (`COOKIE_MAX_AGE`). Future timestamps are also rejected. +- **HTTP-only Cookies**: Tokens are stored in HTTP-only cookies with `SameSite=Strict` to prevent XSS and CSRF attacks. + +#### Timing-Safe Comparisons + +All password and token validations use `crypto.timingSafeEqual` to prevent timing attacks: +- `validatePassword()` in `lib/auth.ts` for login +- `validateAuthToken()` in `lib/auth.ts` for session validation +- `timingSafeCompare()` in `middleware.ts` for Edge runtime + +#### Key Files + +- `middleware.ts` - Route protection and token validation (Edge runtime) +- `lib/auth.ts` - Token generation, validation, and password checking +- `pages/p/[id]/index.tsx` - Admin dashboard with `getServerSideProps` auth check +- `pages/p/[id]/login.tsx` - Login page +- `pages/api/p/[id]/auth.ts` - Login API endpoint +- `pages/api/p/[id]/logout.ts` - Logout API endpoint + +**Why this approach**: The source code is public, so we can't hardcode secret paths. By using environment variables and dynamic routes with server-side validation: +- Scanning the codebase reveals nothing about the admin URL +- Brute-forcing `/p/{random}` returns 404 for incorrect guesses +- API enumeration doesn't reveal admin-related endpoints +- Even finding the path requires knowing the password -The `upsertBooksAndAuthors` function in `pages/admin.tsx`: -- Upserts books by title -- Upserts authors by name -- Creates many-to-many relationships using Prisma's `connect` -- Runs sequentially (not transactional) +**Important**: `ADMIN_PATH`, `ADMIN_PASSWORD`, and `AUTH_SECRET` have no fallback values. The app will error if they're not set, ensuring secure defaults. diff --git a/README.md b/README.md index 02695bc..4fe54f0 100644 --- a/README.md +++ b/README.md @@ -1 +1,100 @@ -This is a starter template for [Learn Next.js](https://nextjs.org/learn). \ No newline at end of file +# Half-Baked + +A personal book collection app that displays books on a visual 3D bookshelf. Built with Next.js, Prisma, and PostgreSQL. + +## Features + +- Visual bookshelf display with 3D shelf effects +- Filter books by date range +- Responsive layout that adapts to screen width +- Admin dashboard for managing books (CRUD operations) +- Protected admin access with password authentication + +## Tech Stack + +- **Framework**: Next.js (Pages Router) +- **Database**: PostgreSQL with Prisma ORM +- **Styling**: Tailwind CSS + SCSS modules +- **Hosting**: Vercel + Supabase + +## Getting Started + +### Prerequisites + +- Node.js 20.x or later +- PostgreSQL database (or Supabase account) + +### Environment Variables + +Create a `.env` file in the root directory: + +```env +DATABASE_URL="postgresql://user:password@host:port/database" +ADMIN_PATH="your-secret-path" +ADMIN_PASSWORD="your-secure-password" +``` + +- `DATABASE_URL`: PostgreSQL connection string +- `ADMIN_PATH`: Secret URL path segment for admin access (e.g., `x7k9m2p4`) +- `ADMIN_PASSWORD`: Password for admin authentication + +### Installation + +```bash +# Install dependencies +npm install + +# Generate Prisma client +npx prisma generate + +# Run database migrations (if using migrations) +npx prisma migrate dev + +# Or sync schema directly (for existing databases) +npx prisma db push +``` + +### Development + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) to view the bookshelf. + +### Admin Access + +The admin dashboard is accessible at `/p/{ADMIN_PATH}` where `{ADMIN_PATH}` is the value of your `ADMIN_PATH` environment variable. + +Example: If `ADMIN_PATH=x7k9m2p4`, access admin at `http://localhost:3000/p/x7k9m2p4` + +### Production Build + +```bash +npm run build +npm run start +``` + +## Project Structure + +``` +pages/ +├── index.tsx # Main bookshelf page +├── p/[id]/ +│ ├── index.tsx # Admin dashboard +│ └── login.tsx # Admin login +├── api/ +│ ├── books/ # Book CRUD endpoints +│ └── p/[id]/ # Auth endpoints +components/ +├── Book/ # Book display component +└── Shelf/ # 3D shelf component +lib/ +└── prisma.ts # Prisma client +prisma/ +└── schema.prisma # Database schema +``` + +## License + +Private project. diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..5d231c6 --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,115 @@ +import crypto from 'crypto'; + +export const AUTH_COOKIE_NAME = 'admin_auth'; +export const COOKIE_MAX_AGE = 60 * 60 * 24; // 24 hours in seconds + +/** + * Generates a signed authentication token using HMAC-SHA256. + * The token is based on a timestamp, making it unique per session. + */ +export function generateAuthToken(): string { + const secret = process.env.AUTH_SECRET; + if (!secret) { + throw new Error('AUTH_SECRET environment variable is not set'); + } + + const timestamp = Date.now().toString(); + const hmac = crypto.createHmac('sha256', secret); + hmac.update(timestamp); + const signature = hmac.digest('hex'); + + // Format: timestamp.signature + return `${timestamp}.${signature}`; +} + +/** + * Validates an authentication token using timing-safe comparison. + * Returns true if the token is valid and not expired. + */ +export function validateAuthToken(token: string | undefined): boolean { + if (!token) { + return false; + } + + const secret = process.env.AUTH_SECRET; + if (!secret) { + return false; + } + + const parts = token.split('.'); + if (parts.length !== 2) { + return false; + } + + const [timestamp, providedSignature] = parts; + + // Check if token is expired or has invalid timestamp + const tokenAge = Date.now() - parseInt(timestamp, 10); + // Reject: NaN, negative (future timestamps), or expired tokens + if (isNaN(tokenAge) || tokenAge < 0 || tokenAge > COOKIE_MAX_AGE * 1000) { + return false; + } + + // Regenerate the expected signature + const hmac = crypto.createHmac('sha256', secret); + hmac.update(timestamp); + const expectedSignature = hmac.digest('hex'); + + // Use timing-safe comparison to prevent timing attacks + return timingSafeEqual(providedSignature, expectedSignature); +} + +/** + * Timing-safe string comparison to prevent timing attacks. + */ +export function timingSafeEqual(a: string, b: string): boolean { + const encoder = new TextEncoder(); + const bufA = encoder.encode(a); + const bufB = encoder.encode(b); + + // If lengths differ, compare with itself to maintain constant time + // but return false + if (bufA.length !== bufB.length) { + crypto.timingSafeEqual(bufA, bufA); + return false; + } + + return crypto.timingSafeEqual(bufA, bufB); +} + +/** + * Validates password using timing-safe comparison. + */ +export function validatePassword(password: string): boolean { + const adminPassword = process.env.ADMIN_PASSWORD; + if (!adminPassword || !password) { + return false; + } + + return timingSafeEqual(password, adminPassword); +} + +/** + * Generates the Set-Cookie header value for authentication. + */ +export function generateAuthCookie(token: string): string { + const secure = process.env.NODE_ENV === 'production' ? '; Secure' : ''; + return `${AUTH_COOKIE_NAME}=${token}; HttpOnly; Path=/; Max-Age=${COOKIE_MAX_AGE}; SameSite=Strict${secure}`; +} + +/** + * Generates the Set-Cookie header value to clear authentication. + */ +export function generateClearAuthCookie(): string { + const secure = process.env.NODE_ENV === 'production' ? '; Secure' : ''; + return `${AUTH_COOKIE_NAME}=; HttpOnly; Path=/; Max-Age=0; SameSite=Strict${secure}`; +} + +/** + * Validates authentication from API request cookies. + * Returns true if the request has a valid auth token. + */ +export function isAuthenticated(cookies: Partial<{ [key: string]: string }>): boolean { + const token = cookies[AUTH_COOKIE_NAME]; + return validateAuthToken(token); +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..3656d9f --- /dev/null +++ b/middleware.ts @@ -0,0 +1,97 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +const AUTH_COOKIE_NAME = 'admin_auth'; +const COOKIE_MAX_AGE = 60 * 60 * 24; // 24 hours in seconds + +/** + * Validates an authentication token in Edge runtime. + * Uses Web Crypto API which is available in Edge. + */ +async function validateAuthToken(token: string | undefined, secret: string): Promise { + if (!token || !secret) { + return false; + } + + const parts = token.split('.'); + if (parts.length !== 2) { + return false; + } + + const [timestamp, providedSignature] = parts; + + // Check if token is expired or has invalid timestamp + const tokenAge = Date.now() - parseInt(timestamp, 10); + // Reject: NaN, negative (future timestamps), or expired tokens + if (isNaN(tokenAge) || tokenAge < 0 || tokenAge > COOKIE_MAX_AGE * 1000) { + return false; + } + + // Regenerate the expected signature using Web Crypto API + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(secret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ); + + const signatureBuffer = await crypto.subtle.sign('HMAC', key, encoder.encode(timestamp)); + const expectedSignature = Array.from(new Uint8Array(signatureBuffer)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + // Timing-safe comparison + if (providedSignature.length !== expectedSignature.length) { + return false; + } + + let result = 0; + for (let i = 0; i < providedSignature.length; i++) { + result |= providedSignature.charCodeAt(i) ^ expectedSignature.charCodeAt(i); + } + + return result === 0; +} + +export async function middleware(request: NextRequest) { + const adminPath = process.env.ADMIN_PATH; + const authSecret = process.env.AUTH_SECRET; + + // If env vars not set, let the request through - pages will handle the error + if (!adminPath || !authSecret) { + console.error('ADMIN_PATH or AUTH_SECRET environment variable is not set'); + return NextResponse.next(); + } + + const { pathname } = request.nextUrl; + const authCookie = request.cookies.get(AUTH_COOKIE_NAME); + + // Protected dashboard route: /p/${ADMIN_PATH} + if (pathname === `/p/${adminPath}`) { + const isValid = await validateAuthToken(authCookie?.value, authSecret); + + if (!isValid) { + // Redirect to login page + const loginUrl = new URL(`/p/${adminPath}/login`, request.url); + return NextResponse.redirect(loginUrl); + } + } + + // If already authenticated and trying to access login page, redirect to dashboard + if (pathname === `/p/${adminPath}/login`) { + const isValid = await validateAuthToken(authCookie?.value, authSecret); + + if (isValid) { + const dashboardUrl = new URL(`/p/${adminPath}`, request.url); + return NextResponse.redirect(dashboardUrl); + } + } + + return NextResponse.next(); +} + +export const config = { + matcher: ['/p/:id*'], +}; diff --git a/package-lock.json b/package-lock.json index e6fbff9..6333913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,10 +4,9 @@ "requires": true, "packages": { "": { - "name": "half-baked", "dependencies": { "@next-auth/prisma-adapter": "^1.0.5", - "@prisma/client": "^4.6.1", + "@prisma/client": "^6.19.2", "@react-hook/resize-observer": "^1.2.6", "next": "latest", "next-auth": "^4.17.0", @@ -18,12 +17,13 @@ "devDependencies": { "@types/node": "18.11.9", "@types/react": "18.0.25", + "@types/react-datepicker": "^6.2.0", "autoprefixer": "^10.4.13", "postcss": "^8.4.18", - "prisma": "^4.6.1", + "prisma": "^6.19.2", "sass": "^1.56.1", "tailwindcss": "^3.2.2", - "typescript": "4.8.4" + "typescript": "^5.9.3" } }, "node_modules/@babel/runtime": { @@ -37,6 +37,64 @@ "node": ">=6.9.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@juggle/resize-observer": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", @@ -304,36 +362,89 @@ } }, "node_modules/@prisma/client": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.6.1.tgz", - "integrity": "sha512-M1+NNrMzqaOIxT7PBGcTs3IZo7d1EW/+gVQd4C4gUgWBDGgD9AcIeZnUSidgWClmpMSgVUdnVORjsWWGUameYA==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", "hasInstallScript": true, - "dependencies": { - "@prisma/engines-version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32" - }, + "license": "Apache-2.0", "engines": { - "node": ">=14.17" + "node": ">=18.18" }, "peerDependencies": { - "prisma": "*" + "prisma": "*", + "typescript": ">=5.1.0" }, "peerDependenciesMeta": { "prisma": { "optional": true + }, + "typescript": { + "optional": true } } }, + "node_modules/@prisma/config": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", + "devOptional": true, + "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.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", + "devOptional": true, + "license": "Apache-2.0" + }, "node_modules/@prisma/engines": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.11.0.tgz", - "integrity": "sha512-0AEBi2HXGV02cf6ASsBPhfsVIbVSDC9nbQed4iiY5eHttW9ZtMxHThuKZE1pnESbr8HRdgmFSa/Kn4OSNYuibg==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", "devOptional": true, - "hasInstallScript": true + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" + } }, "node_modules/@prisma/engines-version": { - "version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32.tgz", - "integrity": "sha512-HUCmkXAU2jqp2O1RvNtbE+seLGLyJGEABZS/R38rZjSAafAy0WzBuHq+tbZMnD+b5OSCsTVtIPVcuvx1ySxcWQ==" + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2" + } }, "node_modules/@react-hook/latest": { "version": "1.0.3", @@ -364,6 +475,13 @@ "react": ">=16.8" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", @@ -395,6 +513,29 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-datepicker": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", + "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.2", + "@types/react": "*", + "date-fns": "^3.3.1" + } + }, + "node_modules/@types/react-datepicker/node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -534,6 +675,65 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "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/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/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==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -597,6 +797,16 @@ "node": ">= 6" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -613,6 +823,23 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -654,6 +881,16 @@ "url": "https://opencollective.com/date-fns" } }, + "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==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/defined": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", @@ -663,6 +900,20 @@ "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==", + "devOptional": true, + "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==", + "devOptional": true, + "license": "MIT" + }, "node_modules/detective": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", @@ -692,12 +943,46 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", "dev": true }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -707,6 +992,36 @@ "node": ">=6" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true, + "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==", + "devOptional": true, + "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-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", @@ -789,6 +1104,24 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "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/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -873,6 +1206,16 @@ "node": ">=0.12.0" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/jose": { "version": "4.14.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", @@ -1061,6 +1404,13 @@ "node": "^10 || ^12 || >=14" } }, + "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==", + "devOptional": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -1085,6 +1435,26 @@ "node": ">=0.10.0" } }, + "node_modules/nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "devOptional": true, + "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/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -1107,6 +1477,13 @@ "node": ">= 6" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", @@ -1143,6 +1520,20 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "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==", + "devOptional": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -1169,6 +1560,18 @@ "node": ">=0.10.0" } }, + "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==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/postcss": { "version": "8.4.18", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", @@ -1322,20 +1725,29 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, "node_modules/prisma": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.11.0.tgz", - "integrity": "sha512-4zZmBXssPUEiX+GeL0MUq/Yyie4ltiKmGu7jCJFnYMamNrrulTBc+D+QwAQSJ01tyzeGHlD13kOnqPwRipnlNw==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", "devOptional": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@prisma/engines": "4.11.0" + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" }, "bin": { - "prisma": "build/index.js", - "prisma2": "build/index.js" + "prisma": "build/index.js" }, "engines": { - "node": ">=14.17" + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/prop-types": { @@ -1348,6 +1760,23 @@ "react-is": "^16.13.1" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "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", @@ -1380,6 +1809,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -1600,6 +2040,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.2.tgz", @@ -1641,6 +2088,16 @@ "postcss": "^8.0.9" } }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1659,16 +2116,17 @@ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "node_modules/typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", - "dev": true, + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/update-browserslist-db": { @@ -1760,6 +2218,51 @@ "regenerator-runtime": "^0.13.11" } }, + "@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "dev": true, + "requires": { + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "dev": true, + "requires": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "dev": true, + "requires": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + } + }, + "@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "dev": true, + "requires": { + "@floating-ui/dom": "^1.7.4" + } + }, + "@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "dev": true + }, "@juggle/resize-observer": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", @@ -1891,23 +2394,66 @@ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@prisma/client": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-4.6.1.tgz", - "integrity": "sha512-M1+NNrMzqaOIxT7PBGcTs3IZo7d1EW/+gVQd4C4gUgWBDGgD9AcIeZnUSidgWClmpMSgVUdnVORjsWWGUameYA==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", + "requires": {} + }, + "@prisma/config": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", + "devOptional": true, "requires": { - "@prisma/engines-version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32" + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" } }, - "@prisma/engines": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-4.11.0.tgz", - "integrity": "sha512-0AEBi2HXGV02cf6ASsBPhfsVIbVSDC9nbQed4iiY5eHttW9ZtMxHThuKZE1pnESbr8HRdgmFSa/Kn4OSNYuibg==", + "@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", "devOptional": true }, + "@prisma/engines": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", + "devOptional": true, + "requires": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" + } + }, "@prisma/engines-version": { - "version": "4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32.tgz", - "integrity": "sha512-HUCmkXAU2jqp2O1RvNtbE+seLGLyJGEABZS/R38rZjSAafAy0WzBuHq+tbZMnD+b5OSCsTVtIPVcuvx1ySxcWQ==" + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "devOptional": true + }, + "@prisma/fetch-engine": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", + "devOptional": true, + "requires": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" + } + }, + "@prisma/get-platform": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", + "devOptional": true, + "requires": { + "@prisma/debug": "6.19.2" + } }, "@react-hook/latest": { "version": "1.0.3", @@ -1931,6 +2477,12 @@ "@react-hook/passive-layout-effect": "^1.2.0" } }, + "@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "devOptional": true + }, "@swc/helpers": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", @@ -1962,6 +2514,25 @@ "csstype": "^3.0.2" } }, + "@types/react-datepicker": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", + "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", + "dev": true, + "requires": { + "@floating-ui/react": "^0.26.2", + "@types/react": "*", + "date-fns": "^3.3.1" + }, + "dependencies": { + "date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "dev": true + } + } + }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", @@ -2048,6 +2619,43 @@ "update-browserslist-db": "^1.0.9" } }, + "c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "requires": { + "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" + }, + "dependencies": { + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "devOptional": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "devOptional": true + } + } + }, "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -2086,6 +2694,15 @@ } } }, + "citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "requires": { + "consola": "^3.2.3" + } + }, "classnames": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", @@ -2102,6 +2719,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true + }, + "consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "devOptional": true + }, "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -2127,12 +2756,30 @@ "@babel/runtime": "^7.21.0" } }, + "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==", + "devOptional": true + }, "defined": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", "dev": true }, + "defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true + }, + "destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true + }, "detective": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", @@ -2156,18 +2803,55 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true + }, + "effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "devOptional": true, + "requires": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", "dev": true }, + "empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, + "exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "devOptional": true + }, + "fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "requires": { + "pure-rand": "^6.1.0" + } + }, "fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", @@ -2229,6 +2913,20 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "requires": { + "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" + } + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2292,6 +2990,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "devOptional": true }, + "jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "devOptional": true + }, "jose": { "version": "4.14.4", "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", @@ -2405,6 +3109,12 @@ "uuid": "^8.3.2" } }, + "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==", + "devOptional": true + }, "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -2423,6 +3133,19 @@ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true }, + "nypm": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "devOptional": true, + "requires": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" + } + }, "oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -2439,6 +3162,12 @@ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true }, + "ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true + }, "oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", @@ -2468,6 +3197,18 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true + }, + "perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -2485,6 +3226,17 @@ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true }, + "pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "devOptional": true, + "requires": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "postcss": { "version": "8.4.18", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", @@ -2570,12 +3322,13 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, "prisma": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-4.11.0.tgz", - "integrity": "sha512-4zZmBXssPUEiX+GeL0MUq/Yyie4ltiKmGu7jCJFnYMamNrrulTBc+D+QwAQSJ01tyzeGHlD13kOnqPwRipnlNw==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", "devOptional": true, "requires": { - "@prisma/engines": "4.11.0" + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" } }, "prop-types": { @@ -2588,6 +3341,12 @@ "react-is": "^16.13.1" } }, + "pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2600,6 +3359,16 @@ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true }, + "rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "requires": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -2742,6 +3511,12 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true + }, "tailwindcss": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.2.tgz", @@ -2773,6 +3548,12 @@ "resolve": "^1.22.1" } }, + "tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "devOptional": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2788,10 +3569,10 @@ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", - "dev": true + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true }, "update-browserslist-db": { "version": "1.0.10", diff --git a/package.json b/package.json index a59b812..ab6d3e3 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "@next-auth/prisma-adapter": "^1.0.5", - "@prisma/client": "^4.6.1", + "@prisma/client": "^6.19.2", "@react-hook/resize-observer": "^1.2.6", "next": "latest", "next-auth": "^4.17.0", @@ -18,12 +18,13 @@ "devDependencies": { "@types/node": "18.11.9", "@types/react": "18.0.25", + "@types/react-datepicker": "^6.2.0", "autoprefixer": "^10.4.13", "postcss": "^8.4.18", - "prisma": "^4.6.1", + "prisma": "^6.19.2", "sass": "^1.56.1", "tailwindcss": "^3.2.2", - "typescript": "4.8.4" + "typescript": "^5.9.3" }, "volta": { "node": "20.18.1" diff --git a/pages/api/books/[id].ts b/pages/api/books/[id].ts index af75acb..e2b9245 100644 --- a/pages/api/books/[id].ts +++ b/pages/api/books/[id].ts @@ -1,5 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import prisma from '@/lib/prisma'; +import { isAuthenticated } from '@/lib/auth'; import { UpdateBookRequest, BookWithAuthors, ApiResponse } from '../../../types/book'; export default async function handler( @@ -19,10 +20,19 @@ export default async function handler( } if (req.method === 'GET') { + // GET is public - allows reading individual book data return handleGet(bookId, req, res); } else if (req.method === 'PUT') { + // PUT requires authentication + if (!isAuthenticated(req.cookies)) { + return res.status(401).json({ success: false, error: 'Unauthorized' }); + } return handlePut(bookId, req, res); } else if (req.method === 'DELETE') { + // DELETE requires authentication + if (!isAuthenticated(req.cookies)) { + return res.status(401).json({ success: false, error: 'Unauthorized' }); + } return handleDelete(bookId, req, res); } else { return res.status(405).json({ success: false, error: 'Method not allowed' }); diff --git a/pages/api/books/index.ts b/pages/api/books/index.ts index 63ed6ff..ab185c7 100644 --- a/pages/api/books/index.ts +++ b/pages/api/books/index.ts @@ -1,5 +1,6 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import prisma from '@/lib/prisma'; +import { isAuthenticated } from '@/lib/auth'; import { CreateBookRequest, BookWithAuthors, ApiResponse } from '../../../types/book'; export default async function handler( @@ -7,8 +8,13 @@ export default async function handler( res: NextApiResponse> ) { if (req.method === 'GET') { + // GET is public - allows the main page to display books return handleGet(req, res); } else if (req.method === 'POST') { + // POST requires authentication + if (!isAuthenticated(req.cookies)) { + return res.status(401).json({ success: false, error: 'Unauthorized' }); + } return handlePost(req, res); } else { return res.status(405).json({ success: false, error: 'Method not allowed' }); diff --git a/pages/api/p/[id]/auth.ts b/pages/api/p/[id]/auth.ts new file mode 100644 index 0000000..3e78c3b --- /dev/null +++ b/pages/api/p/[id]/auth.ts @@ -0,0 +1,51 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { + validatePassword, + generateAuthToken, + generateAuthCookie, +} from '@/lib/auth'; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + // Normalize id to string (req.query.id can be string | string[] | undefined) + const rawId = req.query.id; + const id = Array.isArray(rawId) ? rawId[0] : rawId; + const adminPath = process.env.ADMIN_PATH; + + if (!adminPath) { + console.error('ADMIN_PATH environment variable is not set'); + return res.status(500).json({ error: 'Server configuration error' }); + } + + if (!process.env.ADMIN_PASSWORD) { + console.error('ADMIN_PASSWORD environment variable is not set'); + return res.status(500).json({ error: 'Server configuration error' }); + } + + if (!process.env.AUTH_SECRET) { + console.error('AUTH_SECRET environment variable is not set'); + return res.status(500).json({ error: 'Server configuration error' }); + } + + // Validate the path matches + if (id !== adminPath) { + return res.status(404).json({ error: 'Not found' }); + } + + const { password } = req.body; + + // Use timing-safe comparison to prevent timing attacks + if (validatePassword(password || '')) { + // Generate a signed token + const token = generateAuthToken(); + + // Set HTTP-only cookie with signed token + res.setHeader('Set-Cookie', generateAuthCookie(token)); + return res.status(200).json({ success: true, redirectTo: `/p/${adminPath}` }); + } + + return res.status(401).json({ error: 'Invalid password' }); +} diff --git a/pages/api/p/[id]/logout.ts b/pages/api/p/[id]/logout.ts new file mode 100644 index 0000000..5283c1c --- /dev/null +++ b/pages/api/p/[id]/logout.ts @@ -0,0 +1,28 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { generateClearAuthCookie } from '@/lib/auth'; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + // Normalize id to string (req.query.id can be string | string[] | undefined) + const rawId = req.query.id; + const id = Array.isArray(rawId) ? rawId[0] : rawId; + const adminPath = process.env.ADMIN_PATH; + + if (!adminPath) { + console.error('ADMIN_PATH environment variable is not set'); + return res.status(500).json({ error: 'Server configuration error' }); + } + + // Validate the path matches + if (id !== adminPath) { + return res.status(404).json({ error: 'Not found' }); + } + + // Clear the auth cookie + res.setHeader('Set-Cookie', generateClearAuthCookie()); + + return res.status(200).json({ success: true, redirectTo: `/p/${adminPath}/login` }); +} diff --git a/pages/admin.tsx b/pages/p/[id]/index.tsx similarity index 64% rename from pages/admin.tsx rename to pages/p/[id]/index.tsx index c8e5582..29297c1 100644 --- a/pages/admin.tsx +++ b/pages/p/[id]/index.tsx @@ -1,14 +1,50 @@ import { useState, useEffect } from 'react'; +import { useRouter } from 'next/router'; import Head from 'next/head'; import Image from 'next/image'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; -import { BookWithAuthors } from '../types/book'; +import { GetServerSideProps } from 'next'; +import { BookWithAuthors } from '../../../types/book'; +import { validateAuthToken, AUTH_COOKIE_NAME } from '@/lib/auth'; type FormMode = { mode: 'create' } | { mode: 'edit'; bookId: number }; type Message = { type: 'success' | 'error'; text: string } | null; -export default function Admin() { +export const getServerSideProps: GetServerSideProps = async (context) => { + const { id } = context.params as { id: string }; + const adminPath = process.env.ADMIN_PATH; + + // If env vars not configured, return 404 to avoid exposing configuration issues + if (!adminPath) { + console.error('ADMIN_PATH environment variable is not set'); + return { notFound: true }; + } + + if (id !== adminPath) { + return { notFound: true }; + } + + // Validate auth cookie (defense in depth - middleware also checks this) + const cookies = context.req.cookies; + const authToken = cookies[AUTH_COOKIE_NAME]; + + if (!validateAuthToken(authToken)) { + return { + redirect: { + destination: `/p/${adminPath}/login`, + permanent: false, + }, + }; + } + + return { props: {} }; +}; + +export default function AdminDashboard() { + const router = useRouter(); + const { id } = router.query; + // Form state const [title, setTitle] = useState(''); const [coverUrl, setCoverUrl] = useState(''); @@ -20,6 +56,20 @@ export default function Admin() { const [books, setBooks] = useState([]); const [message, setMessage] = useState(null); const [loading, setLoading] = useState(false); + const [booksLoading, setBooksLoading] = useState(true); + const [booksError, setBooksError] = useState(false); + + async function handleLogout() { + try { + const response = await fetch(`/api/p/${id}/logout`, { method: 'POST' }); + const data = await response.json(); + if (data.redirectTo) { + router.push(data.redirectTo); + } + } catch (error) { + setMessage({ type: 'error', text: 'Failed to logout' }); + } + } // Fetch books on mount useEffect(() => { @@ -35,16 +85,20 @@ export default function Admin() { }, [message]); async function fetchBooks() { + setBooksLoading(true); + setBooksError(false); try { const response = await fetch('/api/books'); const data = await response.json(); if (data.success) { setBooks(data.data); } else { - setMessage({ type: 'error', text: data.error || 'Failed to fetch books' }); + setBooksError(true); } } catch (error) { - setMessage({ type: 'error', text: 'Network error while fetching books' }); + setBooksError(true); + } finally { + setBooksLoading(false); } } @@ -152,12 +206,61 @@ export default function Admin() { return (
- Admin - Book Management + Book Management
-

Book Management

+
+

Book Management

+
+ + + Home + + +
+
{/* Message Banner */} {message && ( @@ -264,10 +367,47 @@ export default function Admin() { {/* Book List Section */}
-

Existing Books ({books.length})

+

+ Existing Books {!booksLoading && !booksError && `(${books.length})`} +

- {books.length === 0 ? ( + {booksLoading ? ( +
+ +

Loading books...

+
+ ) : booksError ? ( +
+

Something went wrong while loading books.

+ +
+ ) : books.length === 0 ? (

No books yet. Add your first book!

) : ( books.map((book) => ( diff --git a/pages/p/[id]/login.tsx b/pages/p/[id]/login.tsx new file mode 100644 index 0000000..fbd63bd --- /dev/null +++ b/pages/p/[id]/login.tsx @@ -0,0 +1,141 @@ +import { useState } from 'react'; +import { useRouter } from 'next/router'; +import Head from 'next/head'; +import { GetServerSideProps } from 'next'; +import { validateAuthToken, AUTH_COOKIE_NAME } from '@/lib/auth'; + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { id } = context.params as { id: string }; + const adminPath = process.env.ADMIN_PATH; + + // If env vars not configured, return 404 to avoid exposing configuration issues + if (!adminPath) { + console.error('ADMIN_PATH environment variable is not set'); + return { notFound: true }; + } + + if (id !== adminPath) { + return { notFound: true }; + } + + // If already authenticated, redirect to dashboard + const cookies = context.req.cookies; + const authToken = cookies[AUTH_COOKIE_NAME]; + + if (validateAuthToken(authToken)) { + return { + redirect: { + destination: `/p/${adminPath}`, + permanent: false, + }, + }; + } + + // Pass the admin path as a prop so client doesn't need router.query + return { props: { adminPath } }; +}; + +interface LoginProps { + adminPath: string; +} + +export default function Login({ adminPath }: LoginProps) { + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const router = useRouter(); + + async function handleSubmit(event: React.FormEvent) { + event.preventDefault(); + setError(''); + setLoading(true); + + try { + const response = await fetch(`/api/p/${adminPath}/auth`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ password }), + }); + + const data = await response.json(); + + if (data.success && data.redirectTo) { + router.push(data.redirectTo); + } else { + setError(data.error || 'Invalid password'); + } + } catch (err) { + setError('Network error. Please try again.'); + } finally { + setLoading(false); + } + } + + return ( +
+ + Login + + + +
+

Login

+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setPassword(e.target.value)} + required + autoFocus + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
+ + +
+ + +
+
+ ); +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f4401e4..567cf31 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,6 +1,3 @@ -// 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" } @@ -11,15 +8,15 @@ datasource db { } model Book { - id Int @id @default(autoincrement()) - title String - authors Author[] - coverUrl String @default("") + id Int @id @default(autoincrement()) + title String + coverUrl String @default("") finishedOn DateTime @default(now()) + authors Author[] @relation("AuthorToBook") } model Author { - id Int @id @default(autoincrement()) - name String @unique - books Book[] + id Int @id @default(autoincrement()) + name String @unique + books Book[] @relation("AuthorToBook") }