From 39b6081baaf1806a98ebda1988fdaad62e28dd85 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Nov 2025 03:52:09 +0000 Subject: [PATCH] Implement comprehensive authentication system (Agent 1) Add complete user authentication with NextAuth.js supporting: - Email/password credentials authentication - OAuth providers (GitHub, Google) with optional configuration - JWT-based session management with 30-day expiry - Role-based access control (USER, GROWER, FARM_MANAGER, ADMIN) - Permission system with granular access control - Secure password hashing with bcrypt (12 rounds) - Rate limiting on auth endpoints - Password reset flow with secure tokens - Email verification system Files added: - lib/auth/: Core auth library (types, permissions, context, hooks, middleware) - pages/api/auth/: Auth API routes (NextAuth, register, forgot-password, verify-email) - pages/auth/: Auth pages (signin, signup, forgot-password, reset-password, verify-email) - components/auth/: Reusable auth components (LoginForm, RegisterForm, AuthGuard, etc.) Updated _app.tsx to include SessionProvider for auth state management. --- .env.example | 10 + bun.lock | 206 ++++++++++++++++++- components/auth/AuthGuard.tsx | 128 ++++++++++++ components/auth/LoginForm.tsx | 132 ++++++++++++ components/auth/PasswordResetForm.tsx | 142 +++++++++++++ components/auth/RegisterForm.tsx | 195 ++++++++++++++++++ components/auth/SocialLoginButtons.tsx | 95 +++++++++ components/auth/index.ts | 5 + lib/auth/AuthContext.tsx | 120 +++++++++++ lib/auth/index.ts | 30 +++ lib/auth/permissions.ts | 107 ++++++++++ lib/auth/types.ts | 76 +++++++ lib/auth/useAuth.ts | 157 ++++++++++++++ lib/auth/withAuth.ts | 180 +++++++++++++++++ lib/auth/withRole.ts | 51 +++++ package.json | 6 + pages/_app.tsx | 15 +- pages/api/auth/[...nextauth].ts | 191 +++++++++++++++++ pages/api/auth/forgot-password.ts | 102 ++++++++++ pages/api/auth/register.ts | 119 +++++++++++ pages/api/auth/reset-password.ts | 106 ++++++++++ pages/api/auth/verify-email.ts | 151 ++++++++++++++ pages/auth/email-verified.tsx | 52 +++++ pages/auth/forgot-password.tsx | 156 ++++++++++++++ pages/auth/reset-password.tsx | 216 ++++++++++++++++++++ pages/auth/signin.tsx | 225 +++++++++++++++++++++ pages/auth/signup.tsx | 270 +++++++++++++++++++++++++ pages/auth/verify-email.tsx | 156 ++++++++++++++ tsconfig.json | 5 +- 29 files changed, 3396 insertions(+), 8 deletions(-) create mode 100644 components/auth/AuthGuard.tsx create mode 100644 components/auth/LoginForm.tsx create mode 100644 components/auth/PasswordResetForm.tsx create mode 100644 components/auth/RegisterForm.tsx create mode 100644 components/auth/SocialLoginButtons.tsx create mode 100644 components/auth/index.ts create mode 100644 lib/auth/AuthContext.tsx create mode 100644 lib/auth/index.ts create mode 100644 lib/auth/permissions.ts create mode 100644 lib/auth/types.ts create mode 100644 lib/auth/useAuth.ts create mode 100644 lib/auth/withAuth.ts create mode 100644 lib/auth/withRole.ts create mode 100644 pages/api/auth/[...nextauth].ts create mode 100644 pages/api/auth/forgot-password.ts create mode 100644 pages/api/auth/register.ts create mode 100644 pages/api/auth/reset-password.ts create mode 100644 pages/api/auth/verify-email.ts create mode 100644 pages/auth/email-verified.tsx create mode 100644 pages/auth/forgot-password.tsx create mode 100644 pages/auth/reset-password.tsx create mode 100644 pages/auth/signin.tsx create mode 100644 pages/auth/signup.tsx create mode 100644 pages/auth/verify-email.tsx diff --git a/.env.example b/.env.example index 666788e..991db0f 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,15 @@ # LocalGreenChain Environment Variables +# NextAuth.js Configuration +NEXTAUTH_URL=http://localhost:3001 +NEXTAUTH_SECRET=your-secret-key-here-generate-with-openssl-rand-base64-32 + +# OAuth Providers (optional) +GITHUB_ID=your_github_client_id +GITHUB_SECRET=your_github_client_secret +GOOGLE_CLIENT_ID=your_google_client_id +GOOGLE_CLIENT_SECRET=your_google_client_secret + # Plants.net API (optional) PLANTS_NET_API_KEY=your_api_key_here diff --git a/bun.lock b/bun.lock index 8bee58b..efaf7e5 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,17 @@ "": { "name": "localgreenchain", "dependencies": { + "@next-auth/prisma-adapter": "^1.0.7", + "@prisma/client": "^7.0.0", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.1", "@tanstack/react-query": "^4.0.10", + "bcryptjs": "^3.0.3", "classnames": "^2.3.1", "drupal-jsonapi-params": "^1.2.2", "html-react-parser": "^1.2.7", "next": "^12.2.3", + "next-auth": "^4.24.13", "next-drupal": "^1.6.0", "nprogress": "^0.2.0", "react": "^17.0.2", @@ -21,6 +25,7 @@ }, "devDependencies": { "@babel/core": "^7.12.9", + "@types/bcryptjs": "^3.0.0", "@types/jest": "^29.5.0", "@types/node": "^17.0.21", "@types/react": "^17.0.0", @@ -28,6 +33,7 @@ "eslint-config-next": "^12.0.10", "jest": "^29.5.0", "postcss": "^8.4.5", + "prisma": "^7.0.0", "tailwindcss": "^3.0.15", "ts-jest": "^29.1.0", "typescript": "^4.5.5", @@ -99,6 +105,8 @@ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="], + "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="], + "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="], "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="], @@ -107,6 +115,20 @@ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@0.2.3", "", {}, "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="], + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], + + "@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], + + "@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], + + "@chevrotain/utils": ["@chevrotain/utils@10.5.0", "", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="], + + "@electric-sql/pglite": ["@electric-sql/pglite@0.3.2", "", {}, "sha512-zfWWa+V2ViDCY/cmUfRqeWY1yLto+EpxjXnZzenB1TyxsTiXaTWeZFIZw6mac52BsuQm0RjCnisjBtdBaXOI6w=="], + + "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.6", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.2" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-6RjmgzphIHIBA4NrMGJsjNWK4pu+bCWJlEWlwcxFTVY3WT86dFpKwbZaGWZV6C5Rd7sCk1Z0CI76QEfukLAUXw=="], + + "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.7", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.2" } }, "sha512-9dAccClqxx4cZB+Ar9B+FZ5WgxDc/Xvl9DPrTWv+dYTf0YNubLzi4wHHRGRGhrJv15XwnyKcGOZAP1VXSneSUg=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -115,6 +137,8 @@ "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + "@hono/node-server": ["@hono/node-server@1.14.2", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GHjpOeHYbr9d1vkID2sNUYkl5IxumyhDrUJB7wBp7jvqYwPFt+oNKsAPBRcdSbV7kIrXhouLE199ks1QcK4r7A=="], + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], @@ -163,6 +187,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.12.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w=="], + + "@next-auth/prisma-adapter": ["@next-auth/prisma-adapter@1.0.7", "", { "peerDependencies": { "@prisma/client": ">=2.26.0 || >=3", "next-auth": "^4" } }, "sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw=="], + "@next/env": ["@next/env@12.3.7", "", {}, "sha512-gCw4sTeHoNr0EUO+Nk9Ll21OzF3PnmM0GlHaKgsY2AWQSqQlMgECvB0YI4k21M9iGy+tQ5RMyXQuoIMpzhtxww=="], "@next/eslint-plugin-next": ["@next/eslint-plugin-next@12.3.7", "", { "dependencies": { "glob": "7.1.7" } }, "sha512-L3WEJJBd1CUUsuxSEThheAV5Nh6/mzCagwj4LHaYlANBkW8Hmg8Ne8l/Vx/sPyfyE7FjuKyiNYWbSVpXRvrmaw=="], @@ -199,6 +227,30 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], + + "@prisma/client": ["@prisma/client@7.0.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.0.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-FM1NtJezl0zH3CybLxcbJwShJt7xFGSRg+1tGhy3sCB8goUDnxnBR+RC/P35EAW8gjkzx7kgz7bvb0MerY2VSw=="], + + "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.0.0", "", {}, "sha512-PAiFgMBPrLSaakBwUpML5NevipuKSL3rtNr8pZ8CZ3OBXo0BFcdeGcBIKw/CxJP6H4GNa4+l5bzJPrk8Iq6tDw=="], + + "@prisma/config": ["@prisma/config@7.0.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-TDASB57hyGUwHB0IPCSkoJcXFrJOKA1+R/1o4np4PbS+E0F5MiY5aAyUttO0mSuNQaX7t8VH/GkDemffF1mQzg=="], + + "@prisma/debug": ["@prisma/debug@7.0.0", "", {}, "sha512-SdS3qzfMASHtWimywtkiRcJtrHzacbmMVhElko3DYUZSB0TTLqRYWpddRBJdeGgSLmy1FD55p7uGzIJ+MtfhMg=="], + + "@prisma/dev": ["@prisma/dev@0.13.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.2", "@electric-sql/pglite-socket": "0.0.6", "@electric-sql/pglite-tools": "0.2.7", "@hono/node-server": "1.14.2", "@mrleebo/prisma-ast": "0.12.1", "@prisma/get-platform": "6.8.2", "@prisma/query-plan-executor": "6.18.0", "foreground-child": "3.3.1", "get-port-please": "3.1.2", "hono": "4.7.10", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.21.3", "std-env": "3.9.0", "valibot": "1.1.0", "zeptomatch": "2.0.2" } }, "sha512-QMmF6zFeUF78yv1HYbHvod83AQnl7u6NtKyDhTRZOJup3h1icWs8R7RUVxBJZvM2tBXNAMpLQYYM/8kPlOPegA=="], + + "@prisma/engines": ["@prisma/engines@7.0.0", "", { "dependencies": { "@prisma/debug": "7.0.0", "@prisma/engines-version": "6.20.0-16.next-0c19ccc313cf9911a90d99d2ac2eb0280c76c513", "@prisma/fetch-engine": "7.0.0", "@prisma/get-platform": "7.0.0" } }, "sha512-ojCL3OFLMCz33UbU9XwH32jwaeM+dWb8cysTuY8eK6ZlMKXJdy6ogrdG3MGB3meKLGdQBmOpUUGJ7eLIaxbrcg=="], + + "@prisma/engines-version": ["@prisma/engines-version@6.20.0-16.next-0c19ccc313cf9911a90d99d2ac2eb0280c76c513", "", {}, "sha512-7bzyN8Gp9GbDFbTDzVUH9nFcgRWvsWmjrGgBJvIC/zEoAuv/lx62gZXgAKfjn/HoPkxz/dS+TtsnduFx8WA+cw=="], + + "@prisma/fetch-engine": ["@prisma/fetch-engine@7.0.0", "", { "dependencies": { "@prisma/debug": "7.0.0", "@prisma/engines-version": "6.20.0-16.next-0c19ccc313cf9911a90d99d2ac2eb0280c76c513", "@prisma/get-platform": "7.0.0" } }, "sha512-qcyWTeWDjVDaDQSrVIymZU1xCYlvmwCzjA395lIuFjUESOH3YQCb8i/hpd4vopfq3fUR4v6+MjjtIGvnmErQgw=="], + + "@prisma/get-platform": ["@prisma/get-platform@6.8.2", "", { "dependencies": { "@prisma/debug": "6.8.2" } }, "sha512-vXSxyUgX3vm1Q70QwzwkjeYfRryIvKno1SXbIqwSptKwqKzskINnDUcx85oX+ys6ooN2ATGSD0xN2UTfg6Zcow=="], + + "@prisma/query-plan-executor": ["@prisma/query-plan-executor@6.18.0", "", {}, "sha512-jZ8cfzFgL0jReE1R10gT8JLHtQxjWYLiQ//wHmVYZ2rVkFHoh0DT8IXsxcKcFlfKN7ak7k6j0XMNn2xVNyr5cA=="], + + "@prisma/studio-core-licensed": ["@prisma/studio-core-licensed@0.8.0", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-SXCcgFvo/SC6/11kEOaQghJgCWNEWZUvPYKn/gpvMB9HLSG/5M8If7dWZtEQHhchvl8bh9A89Hw6mEKpsXFimA=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="], @@ -209,6 +261,8 @@ "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@swc/helpers": ["@swc/helpers@0.4.11", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw=="], "@tailwindcss/forms": ["@tailwindcss/forms@0.4.1", "", { "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1" } }, "sha512-gS9xjCmJjUBz/eP12QlENPLnf0tCx68oYE3mri0GMP5jdtVwLbGUNSRpjsp6NzLAZzZy3ueOwrcqB78Ax6Z84A=="], @@ -227,6 +281,8 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="], + "@types/graceful-fs": ["@types/graceful-fs@4.1.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ=="], "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], @@ -315,6 +371,8 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], @@ -333,6 +391,8 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.8.30", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA=="], + "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], @@ -347,6 +407,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "c12": ["c12@3.1.0", "", { "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" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -365,10 +427,14 @@ "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + "chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], @@ -389,8 +455,14 @@ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + "create-jest": ["create-jest@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" }, "bin": { "create-jest": "bin/create-jest.js" } }, "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -415,10 +487,18 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + "detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="], "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], @@ -439,16 +519,22 @@ "domutils": ["domutils@2.8.0", "", { "dependencies": { "dom-serializer": "^1.0.1", "domelementtype": "^2.2.0", "domhandler": "^4.2.0" } }, "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "drupal-jsonapi-params": ["drupal-jsonapi-params@1.2.3", "", { "dependencies": { "qs": "^6.10.0" } }, "sha512-ZyPXlJkwnNoQ8ERtJiPKY44UzdZDt2RF5NJdh+7UQywx/Q+e7Cu6pHtRs3MJUPEcPUV0dN3jiqCupzBsTGgjmA=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.259", "", {}, "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ=="], "emittery": ["emittery@0.13.1", "", {}, "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ=="], "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + "entities": ["entities@3.0.1", "", {}, "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="], "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], @@ -513,6 +599,10 @@ "expect": ["expect@29.7.0", "", { "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", "jest-matcher-utils": "^29.7.0", "jest-message-util": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw=="], + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -539,6 +629,8 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], @@ -551,6 +643,8 @@ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], @@ -561,12 +655,16 @@ "get-package-type": ["get-package-type@0.1.0", "", {}, "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q=="], + "get-port-please": ["get-port-please@3.1.2", "", {}, "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ=="], + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + "giget": ["giget@2.0.0", "", { "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" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + "glob": ["glob@7.1.7", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -581,6 +679,8 @@ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "grammex": ["grammex@3.1.11", "", {}, "sha512-HNwLkgRg9SqTAd1N3Uh/MnKwTBTzwBxTOPbXQ8pb0tpwydjk90k4zRE8JUn9fMUiRwKtXFZ1TWFmms3dZHN+Fg=="], + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], "handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="], @@ -599,6 +699,8 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hono": ["hono@4.7.10", "", {}, "sha512-QkACju9MiN59CKSY5JsGZCYmPZkA6sIW6OFCUp7qDjZu6S6KHtJHhAc9Uy9mV9F8PJ1/HQ3ybZF2yjCa/73fvQ=="], + "html-dom-parser": ["html-dom-parser@1.2.0", "", { "dependencies": { "domhandler": "4.3.1", "htmlparser2": "7.2.0" } }, "sha512-2HIpFMvvffsXHFUFjso0M9LqM+1Lm22BF+Df2ba+7QHJXjk63pWChEnI6YG27eaWqUdfnh5/Vy+OXrNTtepRsg=="], "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], @@ -607,8 +709,12 @@ "htmlparser2": ["htmlparser2@7.2.0", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.2", "domutils": "^2.8.0", "entities": "^3.0.1" } }, "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog=="], + "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], + "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -669,6 +775,8 @@ "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], @@ -759,6 +867,8 @@ "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -797,14 +907,20 @@ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="], + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], "make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="], @@ -829,8 +945,12 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -839,10 +959,14 @@ "next": ["next@12.3.7", "", { "dependencies": { "@next/env": "12.3.7", "@swc/helpers": "0.4.11", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.14", "styled-jsx": "5.0.7", "use-sync-external-store": "1.2.0" }, "optionalDependencies": { "@next/swc-android-arm-eabi": "12.3.4", "@next/swc-android-arm64": "12.3.4", "@next/swc-darwin-arm64": "12.3.4", "@next/swc-darwin-x64": "12.3.4", "@next/swc-freebsd-x64": "12.3.4", "@next/swc-linux-arm-gnueabihf": "12.3.4", "@next/swc-linux-arm64-gnu": "12.3.4", "@next/swc-linux-arm64-musl": "12.3.4", "@next/swc-linux-x64-gnu": "12.3.4", "@next/swc-linux-x64-musl": "12.3.4", "@next/swc-win32-arm64-msvc": "12.3.4", "@next/swc-win32-ia32-msvc": "12.3.4", "@next/swc-win32-x64-msvc": "12.3.4" }, "peerDependencies": { "fibers": ">= 3.1.0", "node-sass": "^6.0.0 || ^7.0.0", "react": "^17.0.2 || ^18.0.0-0", "react-dom": "^17.0.2 || ^18.0.0-0", "sass": "^1.3.0" }, "optionalPeers": ["fibers", "node-sass", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-3PDn+u77s5WpbkUrslBP6SKLMeUj9cSx251LOt+yP9fgnqXV/ydny81xQsclz9R6RzCLONMCtwK2RvDdLa/mJQ=="], + "next-auth": ["next-auth@4.24.13", "", { "dependencies": { "@babel/runtime": "^7.20.13", "@panva/hkdf": "^1.0.2", "cookie": "^0.7.0", "jose": "^4.15.5", "oauth": "^0.9.15", "openid-client": "^5.4.0", "preact": "^10.6.3", "preact-render-to-string": "^5.1.19", "uuid": "^8.3.2" }, "peerDependencies": { "@auth/core": "0.34.3", "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", "nodemailer": "^7.0.7", "react": "^17.0.2 || ^18 || ^19", "react-dom": "^17.0.2 || ^18 || ^19" }, "optionalPeers": ["@auth/core", "nodemailer"] }, "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ=="], + "next-drupal": ["next-drupal@1.6.0", "", { "dependencies": { "jsona": "^1.9.7", "next": "^12.2.0 || ^13", "node-cache": "^5.1.2", "qs": "^6.10.3", "react": "^17.0.2 || ^18", "react-dom": "^17.0.2 || ^18" } }, "sha512-IRHgcpidXj45jicVl2wEp2WhyaV384rfubxxWopgbmo4YKYvIrg0GtPj3EQNuuX5/EJxyZcULHmmhSXFSidlpg=="], "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -855,6 +979,10 @@ "nprogress": ["nprogress@0.2.0", "", {}, "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="], + "nypm": ["nypm@0.6.2", "", { "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" } }, "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g=="], + + "oauth": ["oauth@0.9.15", "", {}, "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], @@ -873,10 +1001,16 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "oidc-token-hash": ["oidc-token-hash@5.2.0", "", {}, "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "openid-client": ["openid-client@5.7.1", "", { "dependencies": { "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" } }, "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], @@ -901,6 +1035,10 @@ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -911,6 +1049,8 @@ "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], @@ -927,14 +1067,24 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], + + "preact": ["preact@10.27.2", "", {}, "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg=="], + + "preact-render-to-string": ["preact-render-to-string@5.2.6", "", { "dependencies": { "pretty-format": "^3.8.0" }, "peerDependencies": { "preact": ">=10" } }, "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "prisma": ["prisma@7.0.0", "", { "dependencies": { "@prisma/config": "7.0.0", "@prisma/dev": "0.13.0", "@prisma/engines": "7.0.0", "@prisma/studio-core-licensed": "0.8.0", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-VZObZ1pQV/OScarYg68RYUx61GpFLH2mJGf9fUX4XxQxTst/6ZK7nkY86CSZ3zBW6U9lKRTsBrZWVz20X5G/KQ=="], + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], @@ -943,6 +1093,8 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + "react": ["react@17.0.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="], "react-dom": ["react-dom@17.0.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "scheduler": "^0.20.2" }, "peerDependencies": { "react": "17.0.2" } }, "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="], @@ -959,8 +1111,12 @@ "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + "regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "remeda": ["remeda@2.21.3", "", { "dependencies": { "type-fest": "^4.39.1" } }, "sha512-XXrZdLA10oEOQhLLzEJEiFFSKi21REGAkHdImIb4rt/XXy8ORGXh5HCcpUOsElfPNDb+X6TA/+wkh+p2KffYmg=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], @@ -971,6 +1127,8 @@ "resolve.exports": ["resolve.exports@2.0.3", "", {}, "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], @@ -983,10 +1141,14 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.20.2", "", { "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" } }, "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ=="], "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], @@ -1005,7 +1167,7 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], @@ -1025,8 +1187,12 @@ "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], + "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], "string-length": ["string-length@4.0.2", "", { "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" } }, "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ=="], @@ -1075,6 +1241,8 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], @@ -1119,8 +1287,12 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], + "valibot": ["valibot@1.1.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw=="], + "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -1153,6 +1325,8 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zeptomatch": ["zeptomatch@2.0.2", "", { "dependencies": { "grammex": "^3.1.10" } }, "sha512-H33jtSKf8Ijtb5BW6wua3G5DhnFjbFML36eFu+VdOoVY4HD9e7ggjqdM6639B+L87rjnR6Y+XeRzBXZdy52B/g=="], + "@istanbuljs/load-nyc-config/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], "@istanbuljs/load-nyc-config/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -1161,6 +1335,14 @@ "@jest/reporters/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "@mrleebo/prisma-ast/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.0.0", "", { "dependencies": { "@prisma/debug": "7.0.0" } }, "sha512-zyhzrAa+y/GfyCzTnuk0D9lfkvDzo7IbsNyuhTqhPu/AN0txm0x26HAR4tJLismla/fHf5fBzYwSivYSzkpakg=="], + + "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.0.0", "", { "dependencies": { "@prisma/debug": "7.0.0" } }, "sha512-zyhzrAa+y/GfyCzTnuk0D9lfkvDzo7IbsNyuhTqhPu/AN0txm0x26HAR4tJLismla/fHf5fBzYwSivYSzkpakg=="], + + "@prisma/get-platform/@prisma/debug": ["@prisma/debug@6.8.2", "", {}, "sha512-4muBSSUwJJ9BYth5N8tqts8JtiLT8QI/RSAzEogwEfpbYGFo9mYsInsVo8dqXdPO2+Rm5OG5q0qWDDE3nyUbVg=="], + "@tailwindcss/typography/postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -1169,6 +1351,10 @@ "babel-plugin-istanbul/istanbul-lib-instrument": ["istanbul-lib-instrument@5.2.1", "", { "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" } }, "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg=="], + "c12/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "c12/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], @@ -1187,6 +1373,8 @@ "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "globals/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], @@ -1207,14 +1395,24 @@ "make-dir/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "named-placeholders/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + "next/postcss": ["postcss@8.4.14", "", { "dependencies": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig=="], "next/use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], + "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], + + "openid-client/object-hash": ["object-hash@2.2.0", "", {}, "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw=="], + "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], + "preact-render-to-string/pretty-format": ["pretty-format@3.8.0", "", {}, "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="], + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], @@ -1233,10 +1431,16 @@ "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "write-file-atomic/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "@istanbuljs/load-nyc-config/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "@istanbuljs/load-nyc-config/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "c12/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "openid-client/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "@istanbuljs/load-nyc-config/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], diff --git a/components/auth/AuthGuard.tsx b/components/auth/AuthGuard.tsx new file mode 100644 index 0000000..3b55465 --- /dev/null +++ b/components/auth/AuthGuard.tsx @@ -0,0 +1,128 @@ +import { useEffect } from 'react' +import { useRouter } from 'next/router' +import { useAuth } from '@/lib/auth/useAuth' +import { UserRole } from '@/lib/auth/types' +import { hasRole, hasPermission } from '@/lib/auth/permissions' + +interface AuthGuardProps { + children: React.ReactNode + requiredRole?: UserRole + requiredPermission?: string + fallback?: React.ReactNode + redirectTo?: string +} + +export function AuthGuard({ + children, + requiredRole, + requiredPermission, + fallback, + redirectTo = '/auth/signin', +}: AuthGuardProps) { + const router = useRouter() + const { user, isAuthenticated, isLoading } = useAuth() + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + const returnUrl = encodeURIComponent(router.asPath) + router.push(`${redirectTo}?callbackUrl=${returnUrl}`) + } + }, [isLoading, isAuthenticated, router, redirectTo]) + + // Show loading state + if (isLoading) { + return ( + fallback || ( +
+
+
+

Loading...

+
+
+ ) + ) + } + + // Not authenticated + if (!isAuthenticated) { + return ( + fallback || ( +
+
+

Redirecting to sign in...

+
+
+ ) + ) + } + + // Check role requirement + if (requiredRole && user) { + if (!hasRole(user.role, requiredRole)) { + return ( +
+
+
403
+

Access Denied

+

+ You don't have permission to access this page. This page requires{' '} + {requiredRole} role or higher. +

+ +
+
+ ) + } + } + + // Check permission requirement + if (requiredPermission && user) { + if (!hasPermission(user.role, requiredPermission)) { + return ( +
+
+
403
+

Access Denied

+

+ You don't have the required permission to access this page. +

+ +
+
+ ) + } + } + + return <>{children} +} + +// Higher-order component version +export function withAuthGuard

( + Component: React.ComponentType

, + options?: { + requiredRole?: UserRole + requiredPermission?: string + fallback?: React.ReactNode + redirectTo?: string + } +) { + return function AuthGuardedComponent(props: P) { + return ( + + + + ) + } +} + +export default AuthGuard diff --git a/components/auth/LoginForm.tsx b/components/auth/LoginForm.tsx new file mode 100644 index 0000000..958e46a --- /dev/null +++ b/components/auth/LoginForm.tsx @@ -0,0 +1,132 @@ +import { useState } from 'react' +import { signIn } from 'next-auth/react' +import Link from 'next/link' + +interface LoginFormProps { + callbackUrl?: string + onSuccess?: () => void + onError?: (error: string) => void +} + +export function LoginForm({ callbackUrl = '/', onSuccess, onError }: LoginFormProps) { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + try { + const result = await signIn('credentials', { + email, + password, + redirect: false, + }) + + if (result?.error) { + const errorMessage = getErrorMessage(result.error) + setError(errorMessage) + onError?.(errorMessage) + } else if (result?.ok) { + onSuccess?.() + if (callbackUrl) { + window.location.href = callbackUrl + } + } + } catch (err) { + const errorMessage = 'An unexpected error occurred' + setError(errorMessage) + onError?.(errorMessage) + } finally { + setIsLoading(false) + } + } + + return ( +

+ {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm" + placeholder="Email address" + /> +
+
+ + setPassword(e.target.value)} + className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm" + placeholder="Password" + /> +
+
+ +
+
+ + +
+ +
+ + + Forgot password? + + +
+
+ + +
+ ) +} + +function getErrorMessage(error: string): string { + const errorMessages: Record = { + CredentialsSignin: 'Invalid email or password', + default: 'An error occurred during sign in', + } + return errorMessages[error] ?? errorMessages.default +} + +export default LoginForm diff --git a/components/auth/PasswordResetForm.tsx b/components/auth/PasswordResetForm.tsx new file mode 100644 index 0000000..0f05828 --- /dev/null +++ b/components/auth/PasswordResetForm.tsx @@ -0,0 +1,142 @@ +import { useState } from 'react' + +interface PasswordResetFormProps { + token: string + onSuccess?: () => void + onError?: (error: string) => void +} + +export function PasswordResetForm({ token, onSuccess, onError }: PasswordResetFormProps) { + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + const validatePassword = (): string | null => { + if (password.length < 8) { + return 'Password must be at least 8 characters long' + } + + const hasUpperCase = /[A-Z]/.test(password) + const hasLowerCase = /[a-z]/.test(password) + const hasNumbers = /\d/.test(password) + + if (!hasUpperCase || !hasLowerCase || !hasNumbers) { + return 'Password must contain uppercase, lowercase, and numbers' + } + + if (password !== confirmPassword) { + return 'Passwords do not match' + } + + return null + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + const validationError = validatePassword() + if (validationError) { + setError(validationError) + setIsLoading(false) + onError?.(validationError) + return + } + + try { + const response = await fetch('/api/auth/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token, password }), + }) + + const data = await response.json() + + if (!response.ok) { + setError(data.message || 'An error occurred') + onError?.(data.message || 'An error occurred') + return + } + + setSuccess(true) + onSuccess?.() + } catch (err) { + const errorMessage = 'An unexpected error occurred' + setError(errorMessage) + onError?.(errorMessage) + } finally { + setIsLoading(false) + } + } + + if (success) { + return ( +
+

Password Reset Successful!

+

Your password has been reset successfully.

+
+ ) + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+
+ + setPassword(e.target.value)} + className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" + placeholder="At least 8 characters" + /> +

+ Must contain uppercase, lowercase, and numbers +

+
+ +
+ + setConfirmPassword(e.target.value)} + className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" + placeholder="Confirm your password" + /> +
+
+ + +
+ ) +} + +export default PasswordResetForm diff --git a/components/auth/RegisterForm.tsx b/components/auth/RegisterForm.tsx new file mode 100644 index 0000000..4d80042 --- /dev/null +++ b/components/auth/RegisterForm.tsx @@ -0,0 +1,195 @@ +import { useState } from 'react' +import { signIn } from 'next-auth/react' + +interface RegisterFormProps { + callbackUrl?: string + onSuccess?: () => void + onError?: (error: string) => void +} + +export function RegisterForm({ callbackUrl = '/', onSuccess, onError }: RegisterFormProps) { + const [formData, setFormData] = useState({ + name: '', + email: '', + password: '', + confirmPassword: '', + }) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + + const handleChange = (e: React.ChangeEvent) => { + setFormData((prev) => ({ + ...prev, + [e.target.name]: e.target.value, + })) + } + + const validateForm = (): string | null => { + if (!formData.email || !formData.password) { + return 'Email and password are required' + } + + if (formData.password.length < 8) { + return 'Password must be at least 8 characters long' + } + + const hasUpperCase = /[A-Z]/.test(formData.password) + const hasLowerCase = /[a-z]/.test(formData.password) + const hasNumbers = /\d/.test(formData.password) + + if (!hasUpperCase || !hasLowerCase || !hasNumbers) { + return 'Password must contain uppercase, lowercase, and numbers' + } + + if (formData.password !== formData.confirmPassword) { + return 'Passwords do not match' + } + + return null + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + const validationError = validateForm() + if (validationError) { + setError(validationError) + setIsLoading(false) + onError?.(validationError) + return + } + + try { + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: formData.name, + email: formData.email, + password: formData.password, + }), + }) + + const data = await response.json() + + if (!response.ok) { + setError(data.message || 'Registration failed') + onError?.(data.message || 'Registration failed') + return + } + + onSuccess?.() + + // Auto sign in after successful registration + const result = await signIn('credentials', { + email: formData.email, + password: formData.password, + redirect: false, + }) + + if (result?.ok && callbackUrl) { + window.location.href = callbackUrl + } + } catch (err) { + const errorMessage = 'An unexpected error occurred' + setError(errorMessage) + onError?.(errorMessage) + } finally { + setIsLoading(false) + } + } + + return ( +
+ {error && ( +
+ {error} +
+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +

+ Must contain uppercase, lowercase, and numbers +

+
+ +
+ + +
+
+ + +
+ ) +} + +export default RegisterForm diff --git a/components/auth/SocialLoginButtons.tsx b/components/auth/SocialLoginButtons.tsx new file mode 100644 index 0000000..52a1706 --- /dev/null +++ b/components/auth/SocialLoginButtons.tsx @@ -0,0 +1,95 @@ +import { signIn } from 'next-auth/react' + +interface SocialLoginButtonsProps { + callbackUrl?: string + providers?: string[] +} + +const providerConfig: Record = { + github: { + name: 'GitHub', + icon: ( + + + + ), + bgColor: 'bg-gray-900 hover:bg-gray-800', + }, + google: { + name: 'Google', + icon: ( + + + + + + + ), + bgColor: 'bg-white hover:bg-gray-50 border border-gray-300', + }, +} + +export function SocialLoginButtons({ callbackUrl = '/', providers = ['github', 'google'] }: SocialLoginButtonsProps) { + const handleSignIn = (providerId: string) => { + signIn(providerId, { callbackUrl }) + } + + const availableProviders = providers.filter((p) => p in providerConfig) + + if (availableProviders.length === 0) { + return null + } + + return ( +
+ {availableProviders.map((providerId) => { + const config = providerConfig[providerId] + const isGoogle = providerId === 'google' + + return ( + + ) + })} +
+ ) +} + +export function SocialDivider() { + return ( +
+
+
+
+
+ Or continue with +
+
+ ) +} + +export default SocialLoginButtons diff --git a/components/auth/index.ts b/components/auth/index.ts new file mode 100644 index 0000000..3b99559 --- /dev/null +++ b/components/auth/index.ts @@ -0,0 +1,5 @@ +export { LoginForm } from './LoginForm' +export { RegisterForm } from './RegisterForm' +export { PasswordResetForm } from './PasswordResetForm' +export { SocialLoginButtons, SocialDivider } from './SocialLoginButtons' +export { AuthGuard, withAuthGuard } from './AuthGuard' diff --git a/lib/auth/AuthContext.tsx b/lib/auth/AuthContext.tsx new file mode 100644 index 0000000..009b695 --- /dev/null +++ b/lib/auth/AuthContext.tsx @@ -0,0 +1,120 @@ +import * as React from 'react' +import { SessionProvider, useSession, signIn, signOut } from 'next-auth/react' +import { Session } from 'next-auth' +import { AuthUser, UserRole } from './types' +import { hasPermission, hasRole } from './permissions' + +interface AuthContextType { + user: AuthUser | null + isAuthenticated: boolean + isLoading: boolean + signIn: typeof signIn + signOut: typeof signOut + hasPermission: (permission: string) => boolean + hasRole: (role: UserRole) => boolean + updateSession: () => Promise +} + +const AuthContext = React.createContext(undefined) + +interface AuthProviderProps { + children: React.ReactNode + session?: Session | null +} + +function AuthProviderContent({ children }: { children: React.ReactNode }) { + const { data: session, status, update } = useSession() + const isLoading = status === 'loading' + const isAuthenticated = status === 'authenticated' + + const user: AuthUser | null = session?.user + ? { + id: session.user.id, + email: session.user.email!, + name: session.user.name, + image: session.user.image, + role: session.user.role || UserRole.USER, + emailVerified: session.user.emailVerified, + } + : null + + const checkPermission = React.useCallback( + (permission: string): boolean => { + if (!user) return false + return hasPermission(user.role, permission) + }, + [user] + ) + + const checkRole = React.useCallback( + (requiredRole: UserRole): boolean => { + if (!user) return false + return hasRole(user.role, requiredRole) + }, + [user] + ) + + const updateSession = React.useCallback(async () => { + await update() + }, [update]) + + const value: AuthContextType = { + user, + isAuthenticated, + isLoading, + signIn, + signOut, + hasPermission: checkPermission, + hasRole: checkRole, + updateSession, + } + + return {children} +} + +export function AuthProvider({ children, session }: AuthProviderProps) { + return ( + + {children} + + ) +} + +export function useAuth(): AuthContextType { + const context = React.useContext(AuthContext) + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider') + } + return context +} + +// Higher-order component for components that require auth +export function withAuth

( + Component: React.ComponentType

, + options?: { requiredRole?: UserRole; fallback?: React.ReactNode } +) { + return function AuthenticatedComponent(props: P) { + const { isAuthenticated, isLoading, user } = useAuth() + + if (isLoading) { + return options?.fallback ||

Loading...
+ } + + if (!isAuthenticated) { + if (typeof window !== 'undefined') { + signIn() + } + return options?.fallback ||
Redirecting to sign in...
+ } + + if (options?.requiredRole && user) { + if (!hasRole(user.role, options.requiredRole)) { + return
Access denied. Insufficient permissions.
+ } + } + + return + } +} + +export { AuthContext } diff --git a/lib/auth/index.ts b/lib/auth/index.ts new file mode 100644 index 0000000..5375fd8 --- /dev/null +++ b/lib/auth/index.ts @@ -0,0 +1,30 @@ +// Types +export * from './types' + +// Permissions +export * from './permissions' + +// Context and hooks +export { AuthProvider, useAuth, withAuth as withAuthComponent, AuthContext } from './AuthContext' +export { useAuth as useAuthHook, usePermission, useRole, useRequireAuth } from './useAuth' + +// API middleware +export { + withAuth, + withRole, + withPermission, + withAnyPermission, + withAllPermissions, + withRateLimit, + checkRateLimit, +} from './withAuth' +export type { AuthenticatedRequest } from './withAuth' + +// Role-based middleware +export { + requireRole, + requireAdmin, + requireFarmManager, + requireGrower, + requireUser, +} from './withRole' diff --git a/lib/auth/permissions.ts b/lib/auth/permissions.ts new file mode 100644 index 0000000..300653e --- /dev/null +++ b/lib/auth/permissions.ts @@ -0,0 +1,107 @@ +import { UserRole } from './types' + +export const ROLE_HIERARCHY: Record = { + [UserRole.USER]: 0, + [UserRole.GROWER]: 1, + [UserRole.FARM_MANAGER]: 2, + [UserRole.ADMIN]: 3, +} + +export const ROLE_PERMISSIONS: Record = { + [UserRole.USER]: [ + 'plants:read', + 'plants:register', + 'transport:read', + 'demand:read', + 'demand:signal', + 'transparency:read', + ], + [UserRole.GROWER]: [ + 'plants:read', + 'plants:write', + 'plants:register', + 'plants:clone', + 'transport:read', + 'transport:write', + 'demand:read', + 'demand:signal', + 'demand:supply', + 'transparency:read', + 'environment:read', + 'environment:write', + ], + [UserRole.FARM_MANAGER]: [ + 'plants:read', + 'plants:write', + 'plants:register', + 'plants:clone', + 'plants:delete', + 'transport:read', + 'transport:write', + 'demand:read', + 'demand:signal', + 'demand:supply', + 'demand:forecast', + 'transparency:read', + 'transparency:write', + 'environment:read', + 'environment:write', + 'vertical-farm:read', + 'vertical-farm:write', + 'vertical-farm:manage', + ], + [UserRole.ADMIN]: [ + 'plants:*', + 'transport:*', + 'demand:*', + 'transparency:*', + 'environment:*', + 'vertical-farm:*', + 'users:*', + 'system:*', + ], +} + +export function hasPermission(role: UserRole, permission: string): boolean { + const permissions = ROLE_PERMISSIONS[role] || [] + + // Check for exact match + if (permissions.includes(permission)) { + return true + } + + // Check for wildcard permissions (e.g., 'plants:*' matches 'plants:read') + const [resource] = permission.split(':') + const wildcardPermission = `${resource}:*` + if (permissions.includes(wildcardPermission)) { + return true + } + + return false +} + +export function hasRole(userRole: UserRole, requiredRole: UserRole): boolean { + return ROLE_HIERARCHY[userRole] >= ROLE_HIERARCHY[requiredRole] +} + +export function hasAnyPermission(role: UserRole, permissions: string[]): boolean { + return permissions.some(permission => hasPermission(role, permission)) +} + +export function hasAllPermissions(role: UserRole, permissions: string[]): boolean { + return permissions.every(permission => hasPermission(role, permission)) +} + +export function getRoleLabel(role: UserRole): string { + const labels: Record = { + [UserRole.USER]: 'User', + [UserRole.GROWER]: 'Grower', + [UserRole.FARM_MANAGER]: 'Farm Manager', + [UserRole.ADMIN]: 'Administrator', + } + return labels[role] || 'Unknown' +} + +export function getAvailableRoles(): UserRole[] { + return Object.values(UserRole) +} diff --git a/lib/auth/types.ts b/lib/auth/types.ts new file mode 100644 index 0000000..9f10cb1 --- /dev/null +++ b/lib/auth/types.ts @@ -0,0 +1,76 @@ +import { DefaultSession, DefaultUser } from 'next-auth' +import { JWT, DefaultJWT } from 'next-auth/jwt' + +export enum UserRole { + USER = 'USER', + GROWER = 'GROWER', + FARM_MANAGER = 'FARM_MANAGER', + ADMIN = 'ADMIN', +} + +export interface AuthUser { + id: string + email: string + name?: string | null + image?: string | null + role: UserRole + emailVerified?: Date | null +} + +declare module 'next-auth' { + interface Session extends DefaultSession { + user: AuthUser + } + + interface User extends DefaultUser { + role: UserRole + emailVerified?: Date | null + } +} + +declare module 'next-auth/jwt' { + interface JWT extends DefaultJWT { + id: string + role: UserRole + emailVerified?: Date | null + } +} + +export interface RegisterInput { + email: string + password: string + name?: string + role?: UserRole +} + +export interface LoginInput { + email: string + password: string +} + +export interface ForgotPasswordInput { + email: string +} + +export interface ResetPasswordInput { + token: string + password: string +} + +export interface VerifyEmailInput { + token: string +} + +export interface AuthResponse { + success: boolean + message: string + user?: AuthUser + error?: string +} + +export interface TokenPayload { + userId: string + email: string + type: 'email_verification' | 'password_reset' + expiresAt: number +} diff --git a/lib/auth/useAuth.ts b/lib/auth/useAuth.ts new file mode 100644 index 0000000..dc03906 --- /dev/null +++ b/lib/auth/useAuth.ts @@ -0,0 +1,157 @@ +import { useSession, signIn, signOut } from 'next-auth/react' +import { useCallback, useMemo } from 'react' +import { AuthUser, UserRole } from './types' +import { hasPermission, hasRole, hasAnyPermission, hasAllPermissions } from './permissions' + +interface UseAuthReturn { + // User state + user: AuthUser | null + isAuthenticated: boolean + isLoading: boolean + + // Auth actions + login: (provider?: string, options?: { callbackUrl?: string }) => Promise + logout: (options?: { callbackUrl?: string }) => Promise + loginWithCredentials: (email: string, password: string, callbackUrl?: string) => Promise + + // Permission checks + can: (permission: string) => boolean + canAny: (permissions: string[]) => boolean + canAll: (permissions: string[]) => boolean + is: (role: UserRole) => boolean + isAtLeast: (role: UserRole) => boolean + + // Session management + refreshSession: () => Promise +} + +export function useAuth(): UseAuthReturn { + const { data: session, status, update } = useSession() + const isLoading = status === 'loading' + const isAuthenticated = status === 'authenticated' + + const user: AuthUser | null = useMemo(() => { + if (!session?.user) return null + return { + id: session.user.id, + email: session.user.email!, + name: session.user.name, + image: session.user.image, + role: session.user.role || UserRole.USER, + emailVerified: session.user.emailVerified, + } + }, [session]) + + const login = useCallback( + async (provider?: string, options?: { callbackUrl?: string }) => { + await signIn(provider, { callbackUrl: options?.callbackUrl || '/' }) + }, + [] + ) + + const logout = useCallback(async (options?: { callbackUrl?: string }) => { + await signOut({ callbackUrl: options?.callbackUrl || '/' }) + }, []) + + const loginWithCredentials = useCallback( + async (email: string, password: string, callbackUrl?: string) => { + const result = await signIn('credentials', { + email, + password, + redirect: false, + }) + + if (result?.error) { + throw new Error(result.error) + } + + if (callbackUrl) { + window.location.href = callbackUrl + } + }, + [] + ) + + const can = useCallback( + (permission: string): boolean => { + if (!user) return false + return hasPermission(user.role, permission) + }, + [user] + ) + + const canAny = useCallback( + (permissions: string[]): boolean => { + if (!user) return false + return hasAnyPermission(user.role, permissions) + }, + [user] + ) + + const canAll = useCallback( + (permissions: string[]): boolean => { + if (!user) return false + return hasAllPermissions(user.role, permissions) + }, + [user] + ) + + const is = useCallback( + (role: UserRole): boolean => { + if (!user) return false + return user.role === role + }, + [user] + ) + + const isAtLeast = useCallback( + (role: UserRole): boolean => { + if (!user) return false + return hasRole(user.role, role) + }, + [user] + ) + + const refreshSession = useCallback(async () => { + await update() + }, [update]) + + return { + user, + isAuthenticated, + isLoading, + login, + logout, + loginWithCredentials, + can, + canAny, + canAll, + is, + isAtLeast, + refreshSession, + } +} + +// Hook for checking a specific permission +export function usePermission(permission: string): boolean { + const { can } = useAuth() + return can(permission) +} + +// Hook for checking a specific role +export function useRole(role: UserRole): boolean { + const { isAtLeast } = useAuth() + return isAtLeast(role) +} + +// Hook for requiring authentication (with redirect) +export function useRequireAuth(options?: { redirectTo?: string }) { + const { isAuthenticated, isLoading } = useAuth() + + if (!isLoading && !isAuthenticated && typeof window !== 'undefined') { + const redirectTo = options?.redirectTo || '/auth/signin' + window.location.href = `${redirectTo}?callbackUrl=${encodeURIComponent(window.location.pathname)}` + } + + return { isAuthenticated, isLoading } +} diff --git a/lib/auth/withAuth.ts b/lib/auth/withAuth.ts new file mode 100644 index 0000000..64ea92a --- /dev/null +++ b/lib/auth/withAuth.ts @@ -0,0 +1,180 @@ +import { NextApiRequest, NextApiResponse, NextApiHandler } from 'next' +import { getServerSession } from 'next-auth/next' +import { authOptions } from '@/pages/api/auth/[...nextauth]' +import { UserRole, AuthUser } from './types' +import { hasPermission, hasRole } from './permissions' + +export interface AuthenticatedRequest extends NextApiRequest { + user: AuthUser +} + +type AuthenticatedHandler = ( + req: AuthenticatedRequest, + res: NextApiResponse +) => Promise | void + +interface WithAuthOptions { + requiredRole?: UserRole + requiredPermission?: string + requiredPermissions?: string[] + requireAll?: boolean // If true, requires all permissions; if false, requires any +} + +export function withAuth( + handler: AuthenticatedHandler, + options?: WithAuthOptions +): NextApiHandler { + return async (req: NextApiRequest, res: NextApiResponse) => { + try { + const session = await getServerSession(req, res, authOptions) + + if (!session?.user) { + return res.status(401).json({ + error: 'Unauthorized', + message: 'You must be signed in to access this resource', + }) + } + + const user: AuthUser = { + id: session.user.id, + email: session.user.email!, + name: session.user.name, + image: session.user.image, + role: session.user.role || UserRole.USER, + emailVerified: session.user.emailVerified, + } + + // Check role requirement + if (options?.requiredRole) { + if (!hasRole(user.role, options.requiredRole)) { + return res.status(403).json({ + error: 'Forbidden', + message: `This resource requires ${options.requiredRole} role or higher`, + }) + } + } + + // Check single permission + if (options?.requiredPermission) { + if (!hasPermission(user.role, options.requiredPermission)) { + return res.status(403).json({ + error: 'Forbidden', + message: `You do not have the required permission: ${options.requiredPermission}`, + }) + } + } + + // Check multiple permissions + if (options?.requiredPermissions && options.requiredPermissions.length > 0) { + const checkFunction = options.requireAll + ? (perms: string[]) => perms.every(p => hasPermission(user.role, p)) + : (perms: string[]) => perms.some(p => hasPermission(user.role, p)) + + if (!checkFunction(options.requiredPermissions)) { + return res.status(403).json({ + error: 'Forbidden', + message: options.requireAll + ? `You need all of these permissions: ${options.requiredPermissions.join(', ')}` + : `You need at least one of these permissions: ${options.requiredPermissions.join(', ')}`, + }) + } + } + + // Add user to request + const authReq = req as AuthenticatedRequest + authReq.user = user + + return handler(authReq, res) + } catch (error) { + console.error('Auth middleware error:', error) + return res.status(500).json({ + error: 'Internal Server Error', + message: 'An error occurred while authenticating your request', + }) + } + } +} + +// Convenience wrapper for role-based protection +export function withRole( + handler: AuthenticatedHandler, + role: UserRole +): NextApiHandler { + return withAuth(handler, { requiredRole: role }) +} + +// Convenience wrapper for permission-based protection +export function withPermission( + handler: AuthenticatedHandler, + permission: string +): NextApiHandler { + return withAuth(handler, { requiredPermission: permission }) +} + +// Convenience wrapper for multiple permissions (any) +export function withAnyPermission( + handler: AuthenticatedHandler, + permissions: string[] +): NextApiHandler { + return withAuth(handler, { requiredPermissions: permissions, requireAll: false }) +} + +// Convenience wrapper for multiple permissions (all) +export function withAllPermissions( + handler: AuthenticatedHandler, + permissions: string[] +): NextApiHandler { + return withAuth(handler, { requiredPermissions: permissions, requireAll: true }) +} + +// Rate limiting helper (basic implementation) +const rateLimitMap = new Map() + +export function checkRateLimit( + identifier: string, + limit: number = 100, + windowMs: number = 60000 +): { allowed: boolean; remaining: number; resetAt: number } { + const now = Date.now() + const record = rateLimitMap.get(identifier) + + if (!record || now > record.resetAt) { + rateLimitMap.set(identifier, { count: 1, resetAt: now + windowMs }) + return { allowed: true, remaining: limit - 1, resetAt: now + windowMs } + } + + if (record.count >= limit) { + return { allowed: false, remaining: 0, resetAt: record.resetAt } + } + + record.count++ + return { allowed: true, remaining: limit - record.count, resetAt: record.resetAt } +} + +export function withRateLimit( + handler: NextApiHandler, + options: { limit?: number; windowMs?: number; keyGenerator?: (req: NextApiRequest) => string } +): NextApiHandler { + const limit = options.limit || 100 + const windowMs = options.windowMs || 60000 + const keyGenerator = options.keyGenerator || ((req) => req.socket.remoteAddress || 'unknown') + + return async (req: NextApiRequest, res: NextApiResponse) => { + const key = keyGenerator(req) + const { allowed, remaining, resetAt } = checkRateLimit(key, limit, windowMs) + + res.setHeader('X-RateLimit-Limit', limit) + res.setHeader('X-RateLimit-Remaining', remaining) + res.setHeader('X-RateLimit-Reset', Math.ceil(resetAt / 1000)) + + if (!allowed) { + return res.status(429).json({ + error: 'Too Many Requests', + message: 'Rate limit exceeded. Please try again later.', + retryAfter: Math.ceil((resetAt - Date.now()) / 1000), + }) + } + + return handler(req, res) + } +} diff --git a/lib/auth/withRole.ts b/lib/auth/withRole.ts new file mode 100644 index 0000000..dc3b68c --- /dev/null +++ b/lib/auth/withRole.ts @@ -0,0 +1,51 @@ +import { NextApiHandler } from 'next' +import { UserRole } from './types' +import { withAuth, AuthenticatedHandler } from './withAuth' + +/** + * Protect an API route requiring a specific role or higher + * + * @example + * // Only admins can access + * export default requireRole(handler, UserRole.ADMIN) + * + * // Growers and above can access + * export default requireRole(handler, UserRole.GROWER) + */ +export function requireRole( + handler: AuthenticatedHandler, + role: UserRole +): NextApiHandler { + return withAuth(handler, { requiredRole: role }) +} + +/** + * Protect an API route requiring admin role + */ +export function requireAdmin(handler: AuthenticatedHandler): NextApiHandler { + return requireRole(handler, UserRole.ADMIN) +} + +/** + * Protect an API route requiring farm manager role or higher + */ +export function requireFarmManager(handler: AuthenticatedHandler): NextApiHandler { + return requireRole(handler, UserRole.FARM_MANAGER) +} + +/** + * Protect an API route requiring grower role or higher + */ +export function requireGrower(handler: AuthenticatedHandler): NextApiHandler { + return requireRole(handler, UserRole.GROWER) +} + +/** + * Protect an API route requiring any authenticated user + */ +export function requireUser(handler: AuthenticatedHandler): NextApiHandler { + return withAuth(handler) +} + +// Re-export for convenience +export { withAuth, withPermission, withAnyPermission, withAllPermissions } from './withAuth' diff --git a/package.json b/package.json index b1350a8..be11419 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,17 @@ "test:e2e:ci": "start-server-and-test 'bun run preview' http://localhost:3001 cy:run" }, "dependencies": { + "@next-auth/prisma-adapter": "^1.0.7", + "@prisma/client": "^7.0.0", "@tailwindcss/forms": "^0.4.0", "@tailwindcss/typography": "^0.5.1", "@tanstack/react-query": "^4.0.10", + "bcryptjs": "^3.0.3", "classnames": "^2.3.1", "drupal-jsonapi-params": "^1.2.2", "html-react-parser": "^1.2.7", "next": "^12.2.3", + "next-auth": "^4.24.13", "next-drupal": "^1.6.0", "nprogress": "^0.2.0", "react": "^17.0.2", @@ -34,6 +38,7 @@ }, "devDependencies": { "@babel/core": "^7.12.9", + "@types/bcryptjs": "^3.0.0", "@types/jest": "^29.5.0", "@types/node": "^17.0.21", "@types/react": "^17.0.0", @@ -41,6 +46,7 @@ "eslint-config-next": "^12.0.10", "jest": "^29.5.0", "postcss": "^8.4.5", + "prisma": "^7.0.0", "tailwindcss": "^3.0.15", "ts-jest": "^29.1.0", "typescript": "^4.5.5" diff --git a/pages/_app.tsx b/pages/_app.tsx index c4b28f3..cfe06d9 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,7 @@ import * as React from "react" import Router from "next/router" import { QueryClient, QueryClientProvider, Hydrate } from "@tanstack/react-query" +import { SessionProvider } from "next-auth/react" import NProgress from "nprogress" import { syncDrupalPreviewRoutes } from "next-drupal" import "nprogress/nprogress.css" @@ -16,16 +17,18 @@ Router.events.on("routeChangeStart", function (path) { Router.events.on("routeChangeComplete", () => NProgress.done()) Router.events.on("routeChangeError", () => NProgress.done()) -export default function App({ Component, pageProps }) { +export default function App({ Component, pageProps: { session, ...pageProps } }) { const queryClientRef = React.useRef() if (!queryClientRef.current) { queryClientRef.current = new QueryClient() } return ( - - - - - + + + + + + + ) } diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts new file mode 100644 index 0000000..f6df773 --- /dev/null +++ b/pages/api/auth/[...nextauth].ts @@ -0,0 +1,191 @@ +import NextAuth, { NextAuthOptions } from 'next-auth' +import CredentialsProvider from 'next-auth/providers/credentials' +import GitHubProvider from 'next-auth/providers/github' +import GoogleProvider from 'next-auth/providers/google' +import bcrypt from 'bcryptjs' +import { UserRole } from '@/lib/auth/types' + +// In-memory user store for MVP (will be replaced with Prisma in Agent 2) +// This simulates database operations +interface StoredUser { + id: string + email: string + name: string | null + passwordHash: string | null + role: UserRole + emailVerified: Date | null + image: string | null + createdAt: Date +} + +// Temporary in-memory store (will be replaced with database) +const users: Map = new Map() + +// Helper to find user by email +function findUserByEmail(email: string): StoredUser | undefined { + for (const user of users.values()) { + if (user.email.toLowerCase() === email.toLowerCase()) { + return user + } + } + return undefined +} + +// Helper to create user +export function createUser(data: { + email: string + name?: string + passwordHash?: string + role?: UserRole +}): StoredUser { + const id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + const user: StoredUser = { + id, + email: data.email.toLowerCase(), + name: data.name || null, + passwordHash: data.passwordHash || null, + role: data.role || UserRole.USER, + emailVerified: null, + image: null, + createdAt: new Date(), + } + users.set(id, user) + return user +} + +// Helper to get user by ID +export function getUserById(id: string): StoredUser | undefined { + return users.get(id) +} + +// Helper to verify password +async function verifyPassword(password: string, hash: string): Promise { + return bcrypt.compare(password, hash) +} + +export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + name: 'Credentials', + credentials: { + email: { label: 'Email', type: 'email', placeholder: 'your@email.com' }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials) { + if (!credentials?.email || !credentials?.password) { + throw new Error('Email and password are required') + } + + const user = findUserByEmail(credentials.email) + + if (!user) { + throw new Error('No user found with this email') + } + + if (!user.passwordHash) { + throw new Error('Please sign in with your OAuth provider') + } + + const isValid = await verifyPassword(credentials.password, user.passwordHash) + + if (!isValid) { + throw new Error('Invalid password') + } + + return { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + emailVerified: user.emailVerified, + image: user.image, + } + }, + }), + ...(process.env.GITHUB_ID && process.env.GITHUB_SECRET + ? [ + GitHubProvider({ + clientId: process.env.GITHUB_ID, + clientSecret: process.env.GITHUB_SECRET, + }), + ] + : []), + ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET + ? [ + GoogleProvider({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }), + ] + : []), + ], + session: { + strategy: 'jwt', + maxAge: 30 * 24 * 60 * 60, // 30 days + }, + jwt: { + maxAge: 30 * 24 * 60 * 60, // 30 days + }, + pages: { + signIn: '/auth/signin', + signOut: '/auth/signout', + error: '/auth/error', + verifyRequest: '/auth/verify-request', + newUser: '/auth/new-user', + }, + callbacks: { + async signIn({ user, account }) { + // Handle OAuth sign-in + if (account?.provider !== 'credentials') { + const existingUser = findUserByEmail(user.email!) + if (!existingUser) { + // Create new user for OAuth sign-in + createUser({ + email: user.email!, + name: user.name || undefined, + role: UserRole.USER, + }) + } + } + return true + }, + async jwt({ token, user, trigger, session }) { + // Initial sign-in + if (user) { + token.id = user.id + token.role = user.role || UserRole.USER + token.emailVerified = user.emailVerified + } + + // Handle session update + if (trigger === 'update' && session) { + token.name = session.name + token.role = session.role + } + + return token + }, + async session({ session, token }) { + if (session.user) { + session.user.id = token.id + session.user.role = token.role + session.user.emailVerified = token.emailVerified + } + return session + }, + }, + events: { + async signIn({ user, isNewUser }) { + console.log(`User signed in: ${user.email}, isNewUser: ${isNewUser}`) + }, + async signOut({ token }) { + console.log(`User signed out: ${token.email}`) + }, + }, + debug: process.env.NODE_ENV === 'development', +} + +export default NextAuth(authOptions) + +// Export helper for use in registration API +export { findUserByEmail, verifyPassword } diff --git a/pages/api/auth/forgot-password.ts b/pages/api/auth/forgot-password.ts new file mode 100644 index 0000000..47d4fff --- /dev/null +++ b/pages/api/auth/forgot-password.ts @@ -0,0 +1,102 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import crypto from 'crypto' +import { findUserByEmail } from './[...nextauth]' +import { AuthResponse, ForgotPasswordInput, TokenPayload } from '@/lib/auth/types' +import { withRateLimit } from '@/lib/auth/withAuth' + +// In-memory token store (will be replaced with database in Agent 2) +const passwordResetTokens = new Map() + +// Token expiry: 1 hour +const TOKEN_EXPIRY_MS = 60 * 60 * 1000 + +function generateResetToken(userId: string, email: string): string { + const token = crypto.randomBytes(32).toString('hex') + const payload: TokenPayload = { + userId, + email, + type: 'password_reset', + expiresAt: Date.now() + TOKEN_EXPIRY_MS, + } + passwordResetTokens.set(token, payload) + return token +} + +export function verifyResetToken(token: string): TokenPayload | null { + const payload = passwordResetTokens.get(token) + if (!payload) return null + if (Date.now() > payload.expiresAt) { + passwordResetTokens.delete(token) + return null + } + return payload +} + +export function invalidateResetToken(token: string): void { + passwordResetTokens.delete(token) +} + +async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ + success: false, + message: 'Method not allowed', + error: 'Only POST requests are accepted', + }) + } + + try { + const { email }: ForgotPasswordInput = req.body + + if (!email) { + return res.status(400).json({ + success: false, + message: 'Email is required', + error: 'VALIDATION_ERROR', + }) + } + + // Always return success to prevent email enumeration attacks + const successResponse: AuthResponse = { + success: true, + message: 'If an account exists with this email, you will receive a password reset link.', + } + + const user = findUserByEmail(email) + if (!user) { + // Return success even if user doesn't exist (security best practice) + return res.status(200).json(successResponse) + } + + // Generate reset token + const resetToken = generateResetToken(user.id, user.email) + + // Build reset URL + const baseUrl = process.env.NEXTAUTH_URL || `http://${req.headers.host}` + const resetUrl = `${baseUrl}/auth/reset-password?token=${resetToken}` + + // TODO: Send email with reset link (will be implemented with Agent 8 - Notifications) + // For now, log the reset URL (in development only) + if (process.env.NODE_ENV === 'development') { + console.log(`Password reset link for ${email}: ${resetUrl}`) + } + + return res.status(200).json(successResponse) + } catch (error) { + console.error('Forgot password error:', error) + return res.status(500).json({ + success: false, + message: 'An error occurred processing your request', + error: 'INTERNAL_ERROR', + }) + } +} + +// Apply rate limiting: 3 requests per minute per IP +export default withRateLimit(handler, { + limit: 3, + windowMs: 60000, +}) diff --git a/pages/api/auth/register.ts b/pages/api/auth/register.ts new file mode 100644 index 0000000..95b8a4f --- /dev/null +++ b/pages/api/auth/register.ts @@ -0,0 +1,119 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import bcrypt from 'bcryptjs' +import { createUser, findUserByEmail } from './[...nextauth]' +import { UserRole, AuthResponse, RegisterInput } from '@/lib/auth/types' +import { withRateLimit } from '@/lib/auth/withAuth' + +const BCRYPT_ROUNDS = 12 // Secure password hashing + +async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ + success: false, + message: 'Method not allowed', + error: 'Only POST requests are accepted', + }) + } + + try { + const { email, password, name, role }: RegisterInput = req.body + + // Validation + if (!email || !password) { + return res.status(400).json({ + success: false, + message: 'Email and password are required', + error: 'VALIDATION_ERROR', + }) + } + + // Email format validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(email)) { + return res.status(400).json({ + success: false, + message: 'Invalid email format', + error: 'INVALID_EMAIL', + }) + } + + // Password strength validation + if (password.length < 8) { + return res.status(400).json({ + success: false, + message: 'Password must be at least 8 characters long', + error: 'WEAK_PASSWORD', + }) + } + + // Check password complexity + const hasUpperCase = /[A-Z]/.test(password) + const hasLowerCase = /[a-z]/.test(password) + const hasNumbers = /\d/.test(password) + + if (!hasUpperCase || !hasLowerCase || !hasNumbers) { + return res.status(400).json({ + success: false, + message: 'Password must contain uppercase, lowercase, and numbers', + error: 'WEAK_PASSWORD', + }) + } + + // Check if user already exists + const existingUser = findUserByEmail(email) + if (existingUser) { + return res.status(409).json({ + success: false, + message: 'An account with this email already exists', + error: 'USER_EXISTS', + }) + } + + // Hash password with bcrypt + const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS) + + // Determine role (default to USER, only admin can assign higher roles) + // In a real app, you'd check if the requester has admin permissions + const userRole = role === UserRole.ADMIN ? UserRole.USER : (role || UserRole.USER) + + // Create user + const user = createUser({ + email: email.toLowerCase(), + name: name || undefined, + passwordHash, + role: userRole, + }) + + // TODO: Send verification email (will be implemented with Agent 8 - Notifications) + console.log(`New user registered: ${user.email}`) + + return res.status(201).json({ + success: true, + message: 'Registration successful. Please sign in.', + user: { + id: user.id, + email: user.email, + name: user.name, + role: user.role, + image: user.image, + emailVerified: user.emailVerified, + }, + }) + } catch (error) { + console.error('Registration error:', error) + return res.status(500).json({ + success: false, + message: 'An error occurred during registration', + error: 'INTERNAL_ERROR', + }) + } +} + +// Apply rate limiting: 5 registrations per minute per IP +export default withRateLimit(handler, { + limit: 5, + windowMs: 60000, +}) diff --git a/pages/api/auth/reset-password.ts b/pages/api/auth/reset-password.ts new file mode 100644 index 0000000..090d2ed --- /dev/null +++ b/pages/api/auth/reset-password.ts @@ -0,0 +1,106 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import bcrypt from 'bcryptjs' +import { verifyResetToken, invalidateResetToken } from './forgot-password' +import { getUserById } from './[...nextauth]' +import { AuthResponse, ResetPasswordInput } from '@/lib/auth/types' +import { withRateLimit } from '@/lib/auth/withAuth' + +const BCRYPT_ROUNDS = 12 + +async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ + success: false, + message: 'Method not allowed', + error: 'Only POST requests are accepted', + }) + } + + try { + const { token, password }: ResetPasswordInput = req.body + + if (!token || !password) { + return res.status(400).json({ + success: false, + message: 'Token and new password are required', + error: 'VALIDATION_ERROR', + }) + } + + // Validate password strength + if (password.length < 8) { + return res.status(400).json({ + success: false, + message: 'Password must be at least 8 characters long', + error: 'WEAK_PASSWORD', + }) + } + + const hasUpperCase = /[A-Z]/.test(password) + const hasLowerCase = /[a-z]/.test(password) + const hasNumbers = /\d/.test(password) + + if (!hasUpperCase || !hasLowerCase || !hasNumbers) { + return res.status(400).json({ + success: false, + message: 'Password must contain uppercase, lowercase, and numbers', + error: 'WEAK_PASSWORD', + }) + } + + // Verify token + const payload = verifyResetToken(token) + if (!payload) { + return res.status(400).json({ + success: false, + message: 'Invalid or expired reset token', + error: 'INVALID_TOKEN', + }) + } + + // Get user + const user = getUserById(payload.userId) + if (!user) { + return res.status(400).json({ + success: false, + message: 'User not found', + error: 'USER_NOT_FOUND', + }) + } + + // Hash new password + const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS) + + // Update user password (in-memory for now) + user.passwordHash = passwordHash + + // Invalidate the used token + invalidateResetToken(token) + + // TODO: Invalidate all existing sessions for this user (security best practice) + // TODO: Send confirmation email + + console.log(`Password reset successful for user: ${user.email}`) + + return res.status(200).json({ + success: true, + message: 'Password reset successful. You can now sign in with your new password.', + }) + } catch (error) { + console.error('Reset password error:', error) + return res.status(500).json({ + success: false, + message: 'An error occurred resetting your password', + error: 'INTERNAL_ERROR', + }) + } +} + +// Apply rate limiting: 5 requests per minute per IP +export default withRateLimit(handler, { + limit: 5, + windowMs: 60000, +}) diff --git a/pages/api/auth/verify-email.ts b/pages/api/auth/verify-email.ts new file mode 100644 index 0000000..38d4f32 --- /dev/null +++ b/pages/api/auth/verify-email.ts @@ -0,0 +1,151 @@ +import type { NextApiRequest, NextApiResponse } from 'next' +import crypto from 'crypto' +import { findUserByEmail, getUserById } from './[...nextauth]' +import { AuthResponse, VerifyEmailInput, TokenPayload } from '@/lib/auth/types' +import { withRateLimit } from '@/lib/auth/withAuth' + +// In-memory token store (will be replaced with database in Agent 2) +const emailVerificationTokens = new Map() + +// Token expiry: 24 hours +const TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000 + +export function generateVerificationToken(userId: string, email: string): string { + const token = crypto.randomBytes(32).toString('hex') + const payload: TokenPayload = { + userId, + email, + type: 'email_verification', + expiresAt: Date.now() + TOKEN_EXPIRY_MS, + } + emailVerificationTokens.set(token, payload) + return token +} + +function verifyToken(token: string): TokenPayload | null { + const payload = emailVerificationTokens.get(token) + if (!payload) return null + if (Date.now() > payload.expiresAt) { + emailVerificationTokens.delete(token) + return null + } + return payload +} + +function invalidateToken(token: string): void { + emailVerificationTokens.delete(token) +} + +async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + // Handle GET request (verify token from email link) + if (req.method === 'GET') { + const { token } = req.query + + if (!token || typeof token !== 'string') { + return res.status(400).json({ + success: false, + message: 'Verification token is required', + error: 'VALIDATION_ERROR', + }) + } + + const payload = verifyToken(token) + if (!payload) { + return res.status(400).json({ + success: false, + message: 'Invalid or expired verification token', + error: 'INVALID_TOKEN', + }) + } + + const user = getUserById(payload.userId) + if (!user) { + return res.status(400).json({ + success: false, + message: 'User not found', + error: 'USER_NOT_FOUND', + }) + } + + // Mark email as verified + user.emailVerified = new Date() + + // Invalidate the used token + invalidateToken(token) + + console.log(`Email verified for user: ${user.email}`) + + // Redirect to success page or return success response + if (req.headers.accept?.includes('text/html')) { + res.redirect(302, '/auth/email-verified') + return + } + + return res.status(200).json({ + success: true, + message: 'Email verified successfully', + }) + } + + // Handle POST request (resend verification email) + if (req.method === 'POST') { + const { email } = req.body + + if (!email) { + return res.status(400).json({ + success: false, + message: 'Email is required', + error: 'VALIDATION_ERROR', + }) + } + + const user = findUserByEmail(email) + if (!user) { + // Return success to prevent email enumeration + return res.status(200).json({ + success: true, + message: 'If an account exists with this email, a verification link has been sent.', + }) + } + + if (user.emailVerified) { + return res.status(400).json({ + success: false, + message: 'Email is already verified', + error: 'ALREADY_VERIFIED', + }) + } + + // Generate new verification token + const verificationToken = generateVerificationToken(user.id, user.email) + + // Build verification URL + const baseUrl = process.env.NEXTAUTH_URL || `http://${req.headers.host}` + const verifyUrl = `${baseUrl}/api/auth/verify-email?token=${verificationToken}` + + // TODO: Send verification email (will be implemented with Agent 8 - Notifications) + if (process.env.NODE_ENV === 'development') { + console.log(`Email verification link for ${email}: ${verifyUrl}`) + } + + return res.status(200).json({ + success: true, + message: 'If an account exists with this email, a verification link has been sent.', + }) + } + + return res.status(405).json({ + success: false, + message: 'Method not allowed', + error: 'Only GET and POST requests are accepted', + }) +} + +// Apply rate limiting: 3 requests per minute per IP +export default withRateLimit(handler, { + limit: 3, + windowMs: 60000, +}) diff --git a/pages/auth/email-verified.tsx b/pages/auth/email-verified.tsx new file mode 100644 index 0000000..87c797c --- /dev/null +++ b/pages/auth/email-verified.tsx @@ -0,0 +1,52 @@ +import Head from 'next/head' +import Link from 'next/link' +import Layout from '@/components/layout' + +export default function EmailVerified() { + return ( + + + Email Verified | LocalGreenChain + + +
+
+
+
+ + + +
+

Email Verified!

+

+ Your email address has been successfully verified. You now have full access to LocalGreenChain. +

+
+ + +
+
+
+ ) +} diff --git a/pages/auth/forgot-password.tsx b/pages/auth/forgot-password.tsx new file mode 100644 index 0000000..3ea87c4 --- /dev/null +++ b/pages/auth/forgot-password.tsx @@ -0,0 +1,156 @@ +import { useState } from 'react' +import { GetServerSideProps } from 'next' +import { getServerSession } from 'next-auth/next' +import Head from 'next/head' +import Link from 'next/link' +import { authOptions } from '../api/auth/[...nextauth]' +import Layout from '@/components/layout' + +export default function ForgotPassword() { + const [email, setEmail] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + try { + const response = await fetch('/api/auth/forgot-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }) + + const data = await response.json() + + if (!response.ok) { + setError(data.message || 'An error occurred') + return + } + + setSuccess(true) + } catch (err) { + setError('An unexpected error occurred') + } finally { + setIsLoading(false) + } + } + + if (success) { + return ( + + + Check Your Email | LocalGreenChain + +
+
+
+

Check Your Email

+

+ If an account exists with that email address, we've sent you a link to reset your password. +

+

+ Didn't receive the email? Check your spam folder or{' '} + +

+
+ + + Back to sign in + + +
+
+
+ ) + } + + return ( + + + Forgot Password | LocalGreenChain + + +
+
+
+

+ Reset your password +

+

+ Enter your email address and we'll send you a link to reset your password. +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" + placeholder="you@example.com" + /> +
+ +
+ +
+ + +
+
+
+
+ ) +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const session = await getServerSession(context.req, context.res, authOptions) + + if (session) { + return { + redirect: { + destination: '/', + permanent: false, + }, + } + } + + return { + props: {}, + } +} diff --git a/pages/auth/reset-password.tsx b/pages/auth/reset-password.tsx new file mode 100644 index 0000000..2e441b7 --- /dev/null +++ b/pages/auth/reset-password.tsx @@ -0,0 +1,216 @@ +import { useState, useEffect } from 'react' +import { GetServerSideProps } from 'next' +import { getServerSession } from 'next-auth/next' +import Head from 'next/head' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { authOptions } from '../api/auth/[...nextauth]' +import Layout from '@/components/layout' + +export default function ResetPassword() { + const router = useRouter() + const { token } = router.query + + const [password, setPassword] = useState('') + const [confirmPassword, setConfirmPassword] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + useEffect(() => { + if (router.isReady && !token) { + setError('Invalid or missing reset token') + } + }, [router.isReady, token]) + + const validatePassword = (): string | null => { + if (password.length < 8) { + return 'Password must be at least 8 characters long' + } + + const hasUpperCase = /[A-Z]/.test(password) + const hasLowerCase = /[a-z]/.test(password) + const hasNumbers = /\d/.test(password) + + if (!hasUpperCase || !hasLowerCase || !hasNumbers) { + return 'Password must contain uppercase, lowercase, and numbers' + } + + if (password !== confirmPassword) { + return 'Passwords do not match' + } + + return null + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + const validationError = validatePassword() + if (validationError) { + setError(validationError) + setIsLoading(false) + return + } + + try { + const response = await fetch('/api/auth/reset-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token, password }), + }) + + const data = await response.json() + + if (!response.ok) { + setError(data.message || 'An error occurred') + return + } + + setSuccess(true) + } catch (err) { + setError('An unexpected error occurred') + } finally { + setIsLoading(false) + } + } + + if (success) { + return ( + + + Password Reset Successful | LocalGreenChain + +
+
+
+

Password Reset Successful!

+

+ Your password has been reset successfully. You can now sign in with your new password. +

+ + + Sign in + + +
+
+
+
+ ) + } + + return ( + + + Reset Password | LocalGreenChain + + +
+
+
+

+ Set new password +

+

+ Enter your new password below. +

+
+ + {error && ( +
+ {error} + {error.includes('Invalid') && ( + + )} +
+ )} + +
+
+
+ + setPassword(e.target.value)} + className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" + placeholder="At least 8 characters" + /> +

+ Must contain uppercase, lowercase, and numbers +

+
+ +
+ + setConfirmPassword(e.target.value)} + className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" + placeholder="Confirm your password" + /> +
+
+ +
+ +
+ + +
+
+
+
+ ) +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const session = await getServerSession(context.req, context.res, authOptions) + + if (session) { + return { + redirect: { + destination: '/', + permanent: false, + }, + } + } + + return { + props: {}, + } +} diff --git a/pages/auth/signin.tsx b/pages/auth/signin.tsx new file mode 100644 index 0000000..a37144d --- /dev/null +++ b/pages/auth/signin.tsx @@ -0,0 +1,225 @@ +import { useState } from 'react' +import { GetServerSideProps } from 'next' +import { getProviders, signIn, getCsrfToken } from 'next-auth/react' +import { getServerSession } from 'next-auth/next' +import Head from 'next/head' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { authOptions } from '../api/auth/[...nextauth]' +import Layout from '@/components/layout' + +interface SignInProps { + providers: Awaited> + csrfToken: string | undefined +} + +export default function SignIn({ providers, csrfToken }: SignInProps) { + const router = useRouter() + const { callbackUrl, error } = router.query + + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [authError, setAuthError] = useState( + error ? getErrorMessage(error as string) : null + ) + + const handleCredentialsSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setAuthError(null) + + try { + const result = await signIn('credentials', { + email, + password, + redirect: false, + }) + + if (result?.error) { + setAuthError(result.error) + } else if (result?.ok) { + router.push((callbackUrl as string) || '/') + } + } catch (err) { + setAuthError('An unexpected error occurred') + } finally { + setIsLoading(false) + } + } + + const handleOAuthSignIn = (providerId: string) => { + signIn(providerId, { callbackUrl: (callbackUrl as string) || '/' }) + } + + return ( + + + Sign In | LocalGreenChain + + +
+
+
+

+ Sign in to your account +

+

+ Or{' '} + + + create a new account + + +

+
+ + {authError && ( +
+ {authError} +
+ )} + +
+ + +
+
+ + setEmail(e.target.value)} + className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm" + placeholder="Email address" + /> +
+
+ + setPassword(e.target.value)} + className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-green-500 focus:border-green-500 focus:z-10 sm:text-sm" + placeholder="Password" + /> +
+
+ +
+
+ + +
+ + +
+ +
+ +
+
+ + {providers && Object.values(providers).filter(p => p.id !== 'credentials').length > 0 && ( + <> +
+
+
+
+
+
+ Or continue with +
+
+ +
+ {Object.values(providers) + .filter((provider) => provider.id !== 'credentials') + .map((provider) => ( + + ))} +
+
+ + )} +
+
+ + ) +} + +function getErrorMessage(error: string): string { + const errorMessages: Record = { + Signin: 'Try signing in with a different account.', + OAuthSignin: 'Try signing in with a different account.', + OAuthCallback: 'Try signing in with a different account.', + OAuthCreateAccount: 'Try signing in with a different account.', + EmailCreateAccount: 'Try signing in with a different account.', + Callback: 'Try signing in with a different account.', + OAuthAccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.', + EmailSignin: 'Check your email address.', + CredentialsSignin: 'Sign in failed. Check the details you provided are correct.', + default: 'Unable to sign in.', + } + return errorMessages[error] ?? errorMessages.default +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const session = await getServerSession(context.req, context.res, authOptions) + + if (session) { + return { + redirect: { + destination: '/', + permanent: false, + }, + } + } + + const providers = await getProviders() + const csrfToken = await getCsrfToken(context) + + return { + props: { + providers: providers ?? null, + csrfToken: csrfToken ?? null, + }, + } +} diff --git a/pages/auth/signup.tsx b/pages/auth/signup.tsx new file mode 100644 index 0000000..a0c4174 --- /dev/null +++ b/pages/auth/signup.tsx @@ -0,0 +1,270 @@ +import { useState } from 'react' +import { GetServerSideProps } from 'next' +import { getServerSession } from 'next-auth/next' +import { signIn } from 'next-auth/react' +import Head from 'next/head' +import Link from 'next/link' +import { useRouter } from 'next/router' +import { authOptions } from '../api/auth/[...nextauth]' +import Layout from '@/components/layout' + +export default function SignUp() { + const router = useRouter() + const { callbackUrl } = router.query + + const [formData, setFormData] = useState({ + name: '', + email: '', + password: '', + confirmPassword: '', + }) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + const handleChange = (e: React.ChangeEvent) => { + setFormData((prev) => ({ + ...prev, + [e.target.name]: e.target.value, + })) + } + + const validateForm = (): string | null => { + if (!formData.email || !formData.password) { + return 'Email and password are required' + } + + if (formData.password.length < 8) { + return 'Password must be at least 8 characters long' + } + + const hasUpperCase = /[A-Z]/.test(formData.password) + const hasLowerCase = /[a-z]/.test(formData.password) + const hasNumbers = /\d/.test(formData.password) + + if (!hasUpperCase || !hasLowerCase || !hasNumbers) { + return 'Password must contain uppercase, lowercase, and numbers' + } + + if (formData.password !== formData.confirmPassword) { + return 'Passwords do not match' + } + + return null + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + const validationError = validateForm() + if (validationError) { + setError(validationError) + setIsLoading(false) + return + } + + try { + const response = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: formData.name, + email: formData.email, + password: formData.password, + }), + }) + + const data = await response.json() + + if (!response.ok) { + setError(data.message || 'Registration failed') + return + } + + setSuccess(true) + + // Auto sign in after successful registration + setTimeout(async () => { + const result = await signIn('credentials', { + email: formData.email, + password: formData.password, + redirect: false, + }) + + if (result?.ok) { + router.push((callbackUrl as string) || '/') + } else { + router.push('/auth/signin') + } + }, 1500) + } catch (err) { + setError('An unexpected error occurred') + } finally { + setIsLoading(false) + } + } + + if (success) { + return ( + + + Registration Successful | LocalGreenChain + +
+
+
+

Registration Successful!

+

Your account has been created. Signing you in...

+
+
+
+
+ ) + } + + return ( + + + Sign Up | LocalGreenChain + + +
+
+
+

+ Create your account +

+

+ Already have an account?{' '} + + + Sign in + + +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+
+ + +
+ +
+ + +
+ +
+ + +

+ Must contain uppercase, lowercase, and numbers +

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

+ By creating an account, you agree to our{' '} + + Terms of Service + {' '} + and{' '} + + Privacy Policy + +

+
+
+
+
+ ) +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const session = await getServerSession(context.req, context.res, authOptions) + + if (session) { + return { + redirect: { + destination: '/', + permanent: false, + }, + } + } + + return { + props: {}, + } +} diff --git a/pages/auth/verify-email.tsx b/pages/auth/verify-email.tsx new file mode 100644 index 0000000..39facc7 --- /dev/null +++ b/pages/auth/verify-email.tsx @@ -0,0 +1,156 @@ +import { useState } from 'react' +import Head from 'next/head' +import Link from 'next/link' +import Layout from '@/components/layout' +import { useAuth } from '@/lib/auth/useAuth' + +export default function VerifyEmail() { + const { user, isAuthenticated } = useAuth() + const [email, setEmail] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setIsLoading(true) + setError(null) + + const emailToVerify = isAuthenticated ? user?.email : email + + if (!emailToVerify) { + setError('Email address is required') + setIsLoading(false) + return + } + + try { + const response = await fetch('/api/auth/verify-email', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: emailToVerify }), + }) + + const data = await response.json() + + if (!response.ok) { + setError(data.message || 'An error occurred') + return + } + + setSuccess(true) + } catch (err) { + setError('An unexpected error occurred') + } finally { + setIsLoading(false) + } + } + + if (success) { + return ( + + + Verification Email Sent | LocalGreenChain + +
+
+
+

Check Your Email

+

+ If an account exists with that email address, we've sent you a verification link. +

+

+ Didn't receive the email? Check your spam folder or{' '} + +

+
+ + + Go to homepage + + +
+
+
+ ) + } + + return ( + + + Verify Email | LocalGreenChain + + +
+
+
+

+ Verify your email +

+

+ {isAuthenticated + ? 'Click the button below to resend a verification email.' + : 'Enter your email address to receive a verification link.'} +

+
+ + {error && ( +
+ {error} +
+ )} + +
+ {isAuthenticated ? ( +
+

+ Logged in as: {user?.email} +

+
+ ) : ( +
+ + setEmail(e.target.value)} + className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm" + placeholder="you@example.com" + /> +
+ )} + +
+ +
+ + +
+
+
+
+ ) +} diff --git a/tsconfig.json b/tsconfig.json index 740eff3..7d7287a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,10 @@ "jsx": "preserve", "baseUrl": "./", "paths": { - "@/components/*": ["src/components/*"], + "@/*": ["./*"], + "@/components/*": ["components/*"], + "@/lib/*": ["lib/*"], + "@/pages/*": ["pages/*"], "@/nodes/*": ["src/nodes/*"], "@/paragraphs/*": ["src/paragraphs/*"], "@/views/*": ["src/views/*"],