From 88a7d01d55d0aeb9e26b21d921c883530ce00b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karb=C3=A9=20Architect?= Date: Fri, 29 May 2026 10:46:54 +0000 Subject: [PATCH] feat(auth): add multi-role NextAuth with role guards Co-Authored-By: Paperclip --- README.md | 17 + package-lock.json | 1029 ++++++++++++++++- package.json | 17 +- .../migration.sql | 292 +++++ prisma/migrations/migration_lock.toml | 1 + prisma/schema.prisma | 242 +++- src/app/admin/page.tsx | 14 + src/app/api/auth/[...nextauth]/route.ts | 3 + src/app/connexion/page.tsx | 54 + src/app/espace-hote/page.tsx | 25 + src/auth.ts | 75 ++ src/lib/authorization.ts | 23 + src/lib/password.ts | 12 + src/lib/prisma.ts | 20 + src/types/next-auth.d.ts | 23 + 15 files changed, 1826 insertions(+), 21 deletions(-) create mode 100644 prisma/migrations/20260529000000_init_karbe_schema/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/app/admin/page.tsx create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/connexion/page.tsx create mode 100644 src/app/espace-hote/page.tsx create mode 100644 src/auth.ts create mode 100644 src/lib/authorization.ts create mode 100644 src/lib/password.ts create mode 100644 src/lib/prisma.ts create mode 100644 src/types/next-auth.d.ts diff --git a/README.md b/README.md index 8ec8b97..8a363e1 100644 --- a/README.md +++ b/README.md @@ -65,3 +65,20 @@ npx prisma migrate dev | `npm run build` | Build de production | | `npm run start` | Démarre le serveur de production | | `npm run lint` | Lance ESLint | + +## Authentification multi-rôles (NextAuth) + +Implémentation de base disponible avec NextAuth (Credentials): + +- Route d'auth: `/api/auth/[...nextauth]` +- Page de connexion: `/connexion` +- Contrôle d'accès par rôle côté serveur via `requireRole()` +- Exemples de pages protégées: + - `/admin` (ADMIN) + - `/espace-hote` (OWNER ou ADMIN) + +Variables d'environnement à définir: + +- `DATABASE_URL` +- `NEXTAUTH_SECRET` (compatibilité) +- `AUTH_SECRET` (recommandé) diff --git a/package-lock.json b/package-lock.json index 47fe47f..3d3a79a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,22 +9,27 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { + "@aws-sdk/client-s3": "^3.1056.0", + "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", "next": "16.2.6", + "next-auth": "^5.0.0-beta.31", + "pg": "^8.21.0", "react": "19.2.4", "react-dom": "19.2.4" }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", + "@types/node": "^20.19.41", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", "dotenv": "^17.4.2", - "eslint": "^9", - "eslint-config-next": "16.2.6", + "eslint": "^9.39.4", + "eslint-config-next": "^16.2.6", "prisma": "^7.8.0", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5.9.3" } }, "node_modules/@alloc/quick-lru": { @@ -40,6 +45,529 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.2.tgz", + "integrity": "sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^7.0.7" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1056.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1056.0.tgz", + "integrity": "sha512-WfMZEM2eC96anIT0RzR/QmhaefWKsNjOHNv09ORBkcA++ULBnm1fRau+KKGEIbr5nDnf9BTG7E7U3oOVklQS8g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/credential-provider-node": "^3.972.46", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.17", + "@aws-sdk/middleware-expect-continue": "^3.972.14", + "@aws-sdk/middleware-flexible-checksums": "^3.974.23", + "@aws-sdk/middleware-location-constraint": "^3.972.11", + "@aws-sdk/middleware-sdk-s3": "^3.972.44", + "@aws-sdk/middleware-ssec": "^3.972.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.30", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.15.tgz", + "integrity": "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@aws-sdk/xml-builder": "^3.972.26", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.5", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.9.tgz", + "integrity": "sha512-P+QGozmXn2mZZI7sDgk+aUm+RTI61MPSFB+Ir2vjEjEbEsE4e7hYtzrDvAUxZy9ko81h53e11+F/GYlvwDkaOQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.41.tgz", + "integrity": "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.43.tgz", + "integrity": "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.45.tgz", + "integrity": "sha512-sJe5ZWibO4s7RWjFQ8Zol76KxoJcIYyEZH1/wxQSBMSIAAxzaJ8cS/ITAaIHWUQvDKQdt18+cJAHKWB7n1Jmrg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/credential-provider-env": "^3.972.41", + "@aws-sdk/credential-provider-http": "^3.972.43", + "@aws-sdk/credential-provider-login": "^3.972.45", + "@aws-sdk/credential-provider-process": "^3.972.41", + "@aws-sdk/credential-provider-sso": "^3.972.45", + "@aws-sdk/credential-provider-web-identity": "^3.972.45", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/credential-provider-imds": "^4.3.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.45.tgz", + "integrity": "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.46.tgz", + "integrity": "sha512-cS4w0jzDRb1jOlkiJS3y80OxddHzkky/MN9k3NYs5jganNKVLjF0lpvjlwS118oGMr3cdAfOlVdo8gLurTSE7w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.41", + "@aws-sdk/credential-provider-http": "^3.972.43", + "@aws-sdk/credential-provider-ini": "^3.972.45", + "@aws-sdk/credential-provider-process": "^3.972.41", + "@aws-sdk/credential-provider-sso": "^3.972.45", + "@aws-sdk/credential-provider-web-identity": "^3.972.45", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/credential-provider-imds": "^4.3.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.41.tgz", + "integrity": "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.45.tgz", + "integrity": "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/token-providers": "3.1056.0", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.45.tgz", + "integrity": "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.17.tgz", + "integrity": "sha512-lbDmWuHenc+kiwCNrxz4MyN6nkxCWyTXPIWuspJN0ibziu+8CXci7vI1bK9MAkwy8cwJOEXNu0gBM5S0uTGRIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.14.tgz", + "integrity": "sha512-3TNFEVGO4sWZj9TEXOCZLzGEctXHnaO4fk2EQ8KVaboTbwHmEPEQrm17Xb9koImUIXEw0sgi2xtHjg7LuTS3rA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.974.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.23.tgz", + "integrity": "sha512-4nPKARo2lfKvQGUt2fPA5NlS/mEohckdxpuC9ecbjVfj7B7NFFYHeTg+Bf5BEQwdn3yRfUIzFiEkPp8Yuaw3wA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/crc64-nvme": "^3.972.9", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.11.tgz", + "integrity": "sha512-hkfspNUP4criAH6ton6BGKgnm5dZx+7bUOy1YqlTfejDeUPAM23D81q/IX+hdlS3KUsfwGz5ADTqZWKBEUpf4A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.44", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.44.tgz", + "integrity": "sha512-8HQsRg1NpX8vR4vNl1E8pyLnqZroq9VSL2vZQVSgBqp6wv6365LzYD08/c9FFh/9FTg7YRc7aTtEmXF0ir/pqg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/signature-v4-multi-region": "^3.996.30", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.11.tgz", + "integrity": "sha512-7PQvGNhtveKlvVqNahqWx5yrwxP7ecwAoB1dYBf8eKwfo2tzzCbNnW+q2nO3N066ktQaB4iBQbDRWtizm+amoQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.13.tgz", + "integrity": "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/signature-v4-multi-region": "^3.996.30", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/fetch-http-handler": "^5.4.5", + "@smithy/node-http-handler": "^4.7.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.30.tgz", + "integrity": "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.9", + "@smithy/signature-v4": "^5.4.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1056.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1056.0.tgz", + "integrity": "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.15", + "@aws-sdk/nested-clients": "^3.997.13", + "@aws-sdk/types": "^3.973.9", + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz", + "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.26.tgz", + "integrity": "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.2", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -1312,6 +1840,18 @@ "node": ">= 10" } }, + "node_modules/@nodable/entities": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", + "integrity": "sha512-Pig3HxDIoMgjdEH8OCf/dkcTmLFjJRjWuq8jSnklu284/TKOPibSRERmOykiwmyXTtv61mP+44f3GMx0tLAyjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1360,6 +1900,27 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@prisma/adapter-pg": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz", + "integrity": "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/driver-adapter-utils": "7.8.0", + "@types/pg": "^8.16.0", + "pg": "^8.16.3", + "postgres-array": "3.0.4" + } + }, "node_modules/@prisma/client": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.8.0.tgz", @@ -1407,7 +1968,6 @@ "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.8.0.tgz", "integrity": "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==", - "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/dev": { @@ -1436,6 +1996,15 @@ "zeptomatch": "2.1.0" } }, + "node_modules/@prisma/driver-adapter-utils": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@prisma/driver-adapter-utils/-/driver-adapter-utils-7.8.0.tgz", + "integrity": "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "7.8.0" + } + }, "node_modules/@prisma/engines": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.8.0.tgz", @@ -1728,6 +2297,126 @@ "dev": true, "license": "MIT" }, + "node_modules/@smithy/core": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.5.tgz", + "integrity": "sha512-Kt8phUg45M15EjhYAbZ+fFikYneijLu9Liugz8ZsYz2i8j0hzGv27LWKpEHYRfvj+LyCOSijpcR/2i8RouV+cA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.5.tgz", + "integrity": "sha512-yiF8xHpdkaTfzLVqFzsP6WvNghEK+qZzLYWFD13L2SsFhbXwBGlxdocKF95qjr7s5lE5NRage+EJFK4mAsx88Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.5.tgz", + "integrity": "sha512-SK3VMeH0fibgdTg2QeB+O4p7Yy/2E5HBOHJeC58FshkDdeuX8lOgO7PfjYfLyPLP1ch55j91cQqKBzDS0mRjSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.5.tgz", + "integrity": "sha512-3dA9TQ+ybRSZ/m0wnbZhiBy4Dezjgq1Ib/ZZrYTpJDBgpoLLU/SDzZc/g0x0MNAdOJe1wPcM+x2PBRmoOur+Sw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.5.tgz", + "integrity": "sha512-QBJKWGqIknH0dc9LWpfH1mkdokAx6iXYN3UcQ3eY6uIEyScuoQAhfl94ge7ozUy9WgFUdE8xsvwBjaYBbWmPNA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.5", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -2063,12 +2752,22 @@ "version": "20.19.41", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/react": { "version": "19.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", @@ -3042,6 +3741,15 @@ "node": ">=6.0.0" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/better-result": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/better-result/-/better-result-2.9.2.tgz", @@ -3049,6 +3757,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", @@ -4304,6 +5018,43 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -5307,6 +6058,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5978,6 +6738,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.31", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.31.tgz", + "integrity": "sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.2" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -6035,6 +6822,15 @@ "node": ">=18" } }, + "node_modules/oauth4webapi": { + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz", + "integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6256,6 +7052,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6287,6 +7098,104 @@ "devOptional": true, "license": "MIT" }, + "node_modules/pg": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-types/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6371,6 +7280,64 @@ "url": "https://github.com/sponsors/porsager" } }, + "node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7012,6 +7979,15 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -7186,6 +8162,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -7514,7 +8502,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -7726,6 +8713,30 @@ "node": ">=0.10.0" } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 51eb93c..f6e1dc5 100644 --- a/package.json +++ b/package.json @@ -10,21 +10,26 @@ "postinstall": "prisma generate" }, "dependencies": { + "@aws-sdk/client-s3": "^3.1056.0", + "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", + "bcryptjs": "^3.0.3", "next": "16.2.6", + "next-auth": "^5.0.0-beta.31", + "pg": "^8.21.0", "react": "19.2.4", "react-dom": "19.2.4" }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@types/node": "^20", - "@types/react": "^19", - "@types/react-dom": "^19", + "@types/node": "^20.19.41", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", "dotenv": "^17.4.2", - "eslint": "^9", - "eslint-config-next": "16.2.6", + "eslint": "^9.39.4", + "eslint-config-next": "^16.2.6", "prisma": "^7.8.0", "tailwindcss": "^4", - "typescript": "^5" + "typescript": "^5.9.3" } } diff --git a/prisma/migrations/20260529000000_init_karbe_schema/migration.sql b/prisma/migrations/20260529000000_init_karbe_schema/migration.sql new file mode 100644 index 0000000..c4b798c --- /dev/null +++ b/prisma/migrations/20260529000000_init_karbe_schema/migration.sql @@ -0,0 +1,292 @@ +-- CreateSchema +CREATE SCHEMA IF NOT EXISTS "public"; + +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('OWNER', 'CE_MANAGER', 'CE_MEMBER', 'TOURIST', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "CarbetStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED'); + +-- CreateEnum +CREATE TYPE "MediaType" AS ENUM ('PHOTO', 'VIDEO'); + +-- CreateEnum +CREATE TYPE "AvailabilityScope" AS ENUM ('PUBLIC', 'CE_ONLY'); + +-- CreateEnum +CREATE TYPE "AvailabilityBlockReason" AS ENUM ('NONE', 'CE_BLOCKED', 'WEEKEND_BLOCKED'); + +-- CreateEnum +CREATE TYPE "BookingStatus" AS ENUM ('PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED'); + +-- CreateEnum +CREATE TYPE "PaymentStatus" AS ENUM ('PENDING', 'AUTHORIZED', 'SUCCEEDED', 'FAILED', 'REFUNDED'); + +-- CreateEnum +CREATE TYPE "SubscriptionStatus" AS ENUM ('TRIAL', 'ACTIVE', 'PAST_DUE', 'CANCELED'); + +-- CreateTable +CREATE TABLE "Organization" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "phone" TEXT, + "role" "UserRole" NOT NULL, + "organizationId" TEXT, + "avatarUrl" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Carbet" ( + "id" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "description" TEXT NOT NULL, + "river" TEXT NOT NULL, + "latitude" DECIMAL(9,6) NOT NULL, + "longitude" DECIMAL(9,6) NOT NULL, + "embarkPoint" TEXT NOT NULL, + "pirogueDurationMin" INTEGER NOT NULL, + "capacity" INTEGER NOT NULL, + "status" "CarbetStatus" NOT NULL DEFAULT 'DRAFT', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Carbet_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Amenity" ( + "id" TEXT NOT NULL, + "key" TEXT NOT NULL, + "label" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Amenity_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CarbetAmenity" ( + "carbetId" TEXT NOT NULL, + "amenityId" TEXT NOT NULL, + + CONSTRAINT "CarbetAmenity_pkey" PRIMARY KEY ("carbetId","amenityId") +); + +-- CreateTable +CREATE TABLE "Media" ( + "id" TEXT NOT NULL, + "carbetId" TEXT NOT NULL, + "type" "MediaType" NOT NULL, + "s3Key" TEXT NOT NULL, + "s3Url" TEXT NOT NULL, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Media_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Availability" ( + "id" TEXT NOT NULL, + "carbetId" TEXT NOT NULL, + "startDate" TIMESTAMP(3) NOT NULL, + "endDate" TIMESTAMP(3) NOT NULL, + "scope" "AvailabilityScope" NOT NULL DEFAULT 'PUBLIC', + "blockReason" "AvailabilityBlockReason" NOT NULL DEFAULT 'NONE', + "isAvailable" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Availability_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Booking" ( + "id" TEXT NOT NULL, + "carbetId" TEXT NOT NULL, + "tenantId" TEXT NOT NULL, + "startDate" TIMESTAMP(3) NOT NULL, + "endDate" TIMESTAMP(3) NOT NULL, + "guestCount" INTEGER NOT NULL, + "status" "BookingStatus" NOT NULL DEFAULT 'PENDING', + "amount" DECIMAL(10,2) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'EUR', + "paymentStatus" "PaymentStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Booking_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Subscription" ( + "id" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "carbetId" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerSubId" TEXT, + "status" "SubscriptionStatus" NOT NULL DEFAULT 'TRIAL', + "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "renewedAt" TIMESTAMP(3), + "canceledAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Review" ( + "id" TEXT NOT NULL, + "bookingId" TEXT NOT NULL, + "carbetId" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "rating" INTEGER NOT NULL, + "comment" TEXT, + "hostResponse" TEXT, + "hostRespondedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Review_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Organization_slug_key" ON "Organization"("slug"); + +-- CreateIndex +CREATE INDEX "Organization_name_idx" ON "Organization"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE INDEX "User_organizationId_idx" ON "User"("organizationId"); + +-- CreateIndex +CREATE INDEX "User_role_idx" ON "User"("role"); + +-- CreateIndex +CREATE UNIQUE INDEX "Carbet_slug_key" ON "Carbet"("slug"); + +-- CreateIndex +CREATE INDEX "Carbet_ownerId_idx" ON "Carbet"("ownerId"); + +-- CreateIndex +CREATE INDEX "Carbet_status_idx" ON "Carbet"("status"); + +-- CreateIndex +CREATE INDEX "Carbet_river_idx" ON "Carbet"("river"); + +-- CreateIndex +CREATE UNIQUE INDEX "Amenity_key_key" ON "Amenity"("key"); + +-- CreateIndex +CREATE INDEX "CarbetAmenity_amenityId_idx" ON "CarbetAmenity"("amenityId"); + +-- CreateIndex +CREATE INDEX "Media_carbetId_sortOrder_idx" ON "Media"("carbetId", "sortOrder"); + +-- CreateIndex +CREATE INDEX "Availability_carbetId_idx" ON "Availability"("carbetId"); + +-- CreateIndex +CREATE INDEX "Availability_scope_blockReason_idx" ON "Availability"("scope", "blockReason"); + +-- CreateIndex +CREATE INDEX "Availability_startDate_endDate_idx" ON "Availability"("startDate", "endDate"); + +-- CreateIndex +CREATE INDEX "Booking_carbetId_idx" ON "Booking"("carbetId"); + +-- CreateIndex +CREATE INDEX "Booking_tenantId_idx" ON "Booking"("tenantId"); + +-- CreateIndex +CREATE INDEX "Booking_status_paymentStatus_idx" ON "Booking"("status", "paymentStatus"); + +-- CreateIndex +CREATE INDEX "Booking_startDate_endDate_idx" ON "Booking"("startDate", "endDate"); + +-- CreateIndex +CREATE UNIQUE INDEX "Subscription_providerSubId_key" ON "Subscription"("providerSubId"); + +-- CreateIndex +CREATE INDEX "Subscription_ownerId_idx" ON "Subscription"("ownerId"); + +-- CreateIndex +CREATE INDEX "Subscription_carbetId_idx" ON "Subscription"("carbetId"); + +-- CreateIndex +CREATE INDEX "Subscription_status_idx" ON "Subscription"("status"); + +-- CreateIndex +CREATE UNIQUE INDEX "Review_bookingId_key" ON "Review"("bookingId"); + +-- CreateIndex +CREATE INDEX "Review_carbetId_idx" ON "Review"("carbetId"); + +-- CreateIndex +CREATE INDEX "Review_authorId_idx" ON "Review"("authorId"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Carbet" ADD CONSTRAINT "Carbet_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CarbetAmenity" ADD CONSTRAINT "CarbetAmenity_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CarbetAmenity" ADD CONSTRAINT "CarbetAmenity_amenityId_fkey" FOREIGN KEY ("amenityId") REFERENCES "Amenity"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Media" ADD CONSTRAINT "Media_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Availability" ADD CONSTRAINT "Availability_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Booking" ADD CONSTRAINT "Booking_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Booking" ADD CONSTRAINT "Booking_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_carbetId_fkey" FOREIGN KEY ("carbetId") REFERENCES "Carbet"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Review" ADD CONSTRAINT "Review_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2fe25d8 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1 @@ +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 44c1455..abe74c4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,8 +1,3 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Get a free hosted Postgres database in seconds: `npx create-db` - generator client { provider = "prisma-client" output = "../src/generated/prisma" @@ -12,4 +7,239 @@ datasource db { provider = "postgresql" } -// Les modèles Karbé (carbets, réservations, utilisateurs…) seront ajoutés ici. +enum UserRole { + OWNER + CE_MANAGER + CE_MEMBER + TOURIST + ADMIN +} + +enum CarbetStatus { + DRAFT + PUBLISHED + ARCHIVED +} + +enum MediaType { + PHOTO + VIDEO +} + +enum AvailabilityScope { + PUBLIC + CE_ONLY +} + +enum AvailabilityBlockReason { + NONE + CE_BLOCKED + WEEKEND_BLOCKED +} + +enum BookingStatus { + PENDING + CONFIRMED + CANCELLED + COMPLETED +} + +enum PaymentStatus { + PENDING + AUTHORIZED + SUCCEEDED + FAILED + REFUNDED +} + +enum SubscriptionStatus { + TRIAL + ACTIVE + PAST_DUE + CANCELED +} + +model Organization { + id String @id @default(cuid()) + name String + slug String @unique + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + members User[] + + @@index([name]) +} + +model User { + id String @id @default(cuid()) + email String @unique + passwordHash String + firstName String + lastName String + phone String? + role UserRole + organizationId String? + avatarUrl String? + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull) + carbets Carbet[] @relation("CarbetOwner") + bookings Booking[] @relation("BookingTenant") + reviews Review[] @relation("ReviewAuthor") + subscriptions Subscription[] + + @@index([organizationId]) + @@index([role]) +} + +model Carbet { + id String @id @default(cuid()) + ownerId String + title String + slug String @unique + description String + river String + latitude Decimal @db.Decimal(9, 6) + longitude Decimal @db.Decimal(9, 6) + embarkPoint String + pirogueDurationMin Int + capacity Int + status CarbetStatus @default(DRAFT) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + owner User @relation("CarbetOwner", fields: [ownerId], references: [id], onDelete: Restrict) + amenities CarbetAmenity[] + media Media[] + availabilities Availability[] + bookings Booking[] + reviews Review[] + subscriptions Subscription[] + + @@index([ownerId]) + @@index([status]) + @@index([river]) +} + +model Amenity { + id String @id @default(cuid()) + key String @unique + label String + description String? + createdAt DateTime @default(now()) + + carbets CarbetAmenity[] +} + +model CarbetAmenity { + carbetId String + amenityId String + + carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade) + amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade) + + @@id([carbetId, amenityId]) + @@index([amenityId]) +} + +model Media { + id String @id @default(cuid()) + carbetId String + type MediaType + s3Key String + s3Url String + sortOrder Int @default(0) + createdAt DateTime @default(now()) + + carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade) + + @@index([carbetId, sortOrder]) +} + +model Availability { + id String @id @default(cuid()) + carbetId String + startDate DateTime + endDate DateTime + scope AvailabilityScope @default(PUBLIC) + blockReason AvailabilityBlockReason @default(NONE) + isAvailable Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade) + + @@index([carbetId]) + @@index([scope, blockReason]) + @@index([startDate, endDate]) +} + +model Booking { + id String @id @default(cuid()) + carbetId String + tenantId String + startDate DateTime + endDate DateTime + guestCount Int + status BookingStatus @default(PENDING) + amount Decimal @db.Decimal(10, 2) + currency String @default("EUR") + paymentStatus PaymentStatus @default(PENDING) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Restrict) + tenant User @relation("BookingTenant", fields: [tenantId], references: [id], onDelete: Restrict) + + review Review? + + @@index([carbetId]) + @@index([tenantId]) + @@index([status, paymentStatus]) + @@index([startDate, endDate]) +} + +model Subscription { + id String @id @default(cuid()) + ownerId String + carbetId String + provider String + providerSubId String? @unique + status SubscriptionStatus @default(TRIAL) + startedAt DateTime @default(now()) + renewedAt DateTime? + canceledAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + owner User @relation(fields: [ownerId], references: [id], onDelete: Restrict) + carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Cascade) + + @@index([ownerId]) + @@index([carbetId]) + @@index([status]) +} + +model Review { + id String @id @default(cuid()) + bookingId String @unique + carbetId String + authorId String + rating Int + comment String? + hostResponse String? + hostRespondedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade) + carbet Carbet @relation(fields: [carbetId], references: [id], onDelete: Restrict) + author User @relation("ReviewAuthor", fields: [authorId], references: [id], onDelete: Restrict) + + @@index([carbetId]) + @@index([authorId]) +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..731159d --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,14 @@ +import { requireRole } from "@/lib/authorization"; + +export default async function AdminPage() { + const session = await requireRole(["ADMIN"]); + + return ( +
+

Espace administrateur

+

+ Accès autorisé pour {session.user.email} ({session.user.role}). +

+
+ ); +} diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..86c9f3d --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "@/auth"; + +export const { GET, POST } = handlers; diff --git a/src/app/connexion/page.tsx b/src/app/connexion/page.tsx new file mode 100644 index 0000000..032a91b --- /dev/null +++ b/src/app/connexion/page.tsx @@ -0,0 +1,54 @@ +import { redirect } from "next/navigation"; + +import { auth, signIn } from "@/auth"; + +export default async function SignInPage() { + const session = await auth(); + if (session?.user?.id) { + redirect("/"); + } + + return ( +
+
{ + "use server"; + await signIn("credentials", formData); + }} + className="w-full space-y-4 rounded-xl border border-zinc-200 bg-white p-6" + > +

Connexion

+
+ + +
+
+ + +
+ +
+
+ ); +} diff --git a/src/app/espace-hote/page.tsx b/src/app/espace-hote/page.tsx new file mode 100644 index 0000000..d412d73 --- /dev/null +++ b/src/app/espace-hote/page.tsx @@ -0,0 +1,25 @@ +import Link from "next/link"; + +import { requireRole } from "@/lib/authorization"; + +export default async function HostPage() { + const session = await requireRole(["OWNER", "ADMIN"]); + + return ( +
+

Espace hôte

+

+ Accès autorisé pour {session.user.email} ({session.user.role}). +

+ +
+ + Gérer mes carbets + +
+
+ ); +} diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..4071c3d --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,75 @@ +import NextAuth from "next-auth"; +import Credentials from "next-auth/providers/credentials"; + +import { prisma } from "@/lib/prisma"; +import { verifyPassword } from "@/lib/password"; + +export const { handlers, auth, signIn, signOut } = NextAuth({ + session: { + strategy: "jwt", + }, + providers: [ + Credentials({ + name: "Email et mot de passe", + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Mot de passe", type: "password" }, + }, + async authorize(credentials) { + const email = credentials?.email?.toString().trim().toLowerCase(); + const password = credentials?.password?.toString() ?? ""; + + if (!email || !password) { + return null; + } + + const user = await prisma.user.findUnique({ + where: { email }, + select: { + id: true, + email: true, + firstName: true, + lastName: true, + role: true, + isActive: true, + passwordHash: true, + }, + }); + + if (!user || !user.isActive) { + return null; + } + + const isValid = await verifyPassword(password, user.passwordHash); + if (!isValid) { + return null; + } + + return { + id: user.id, + email: user.email, + name: `${user.firstName} ${user.lastName}`.trim(), + role: user.role, + }; + }, + }), + ], + callbacks: { + async jwt({ token, user }) { + if (user?.role) { + token.role = user.role; + } + return token; + }, + async session({ session, token }) { + if (session.user) { + session.user.id = token.sub ?? ""; + session.user.role = token.role; + } + return session; + }, + }, + pages: { + signIn: "/connexion", + }, +}); diff --git a/src/lib/authorization.ts b/src/lib/authorization.ts new file mode 100644 index 0000000..4841b1d --- /dev/null +++ b/src/lib/authorization.ts @@ -0,0 +1,23 @@ +import { redirect } from "next/navigation"; + +import { auth } from "@/auth"; +import type { UserRole } from "@/generated/prisma/enums"; + +export async function requireAuth() { + const session = await auth(); + if (!session?.user?.id) { + redirect("/connexion"); + } + + return session; +} + +export async function requireRole(allowedRoles: UserRole[]) { + const session = await requireAuth(); + + if (!session.user.role || !allowedRoles.includes(session.user.role)) { + redirect("/"); + } + + return session; +} diff --git a/src/lib/password.ts b/src/lib/password.ts new file mode 100644 index 0000000..78d7cb4 --- /dev/null +++ b/src/lib/password.ts @@ -0,0 +1,12 @@ +import bcrypt from "bcryptjs"; + +export async function verifyPassword( + plainTextPassword: string, + hashedPassword: string, +): Promise { + return bcrypt.compare(plainTextPassword, hashedPassword); +} + +export async function hashPassword(plainTextPassword: string): Promise { + return bcrypt.hash(plainTextPassword, 12); +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..e4c6e99 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,20 @@ +import { PrismaPg } from "@prisma/adapter-pg"; + +import { PrismaClient } from "@/generated/prisma/client"; + +const connectionString = process.env.DATABASE_URL; +if (!connectionString) { + throw new Error("DATABASE_URL is required"); +} + +const globalForPrisma = globalThis as unknown as { + prisma?: PrismaClient; +}; + +const adapter = new PrismaPg({ connectionString }); + +export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter }); + +if (process.env.NODE_ENV !== "production") { + globalForPrisma.prisma = prisma; +} diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts new file mode 100644 index 0000000..d470fff --- /dev/null +++ b/src/types/next-auth.d.ts @@ -0,0 +1,23 @@ +import type { DefaultSession } from "next-auth"; +import type { JWT as DefaultJWT } from "next-auth/jwt"; + +import type { UserRole } from "@/generated/prisma/enums"; + +declare module "next-auth" { + interface Session { + user: { + id: string; + role?: UserRole; + } & DefaultSession["user"]; + } + + interface User { + role?: UserRole; + } +} + +declare module "next-auth/jwt" { + interface JWT extends DefaultJWT { + role?: UserRole; + } +}