feat: integrate NextAuth for authentication, refactor login and registration processes, and enhance middleware for session management

This commit is contained in:
Julien Froidefond
2025-10-16 15:50:37 +02:00
parent 9ecdd72804
commit 7426bfb33c
33 changed files with 417 additions and 729 deletions

24
ENV.md Normal file
View File

@@ -0,0 +1,24 @@
# Variables d'environnement requises
## Production (.env)
```env
# MongoDB Configuration
MONGO_USER=admin
MONGO_PASSWORD=your-secure-password
MONGODB_URI=mongodb://admin:your-secure-password@mongodb:27017/stripstream?authSource=admin
# NextAuth Configuration
NEXTAUTH_SECRET=your-secret-key-here-generate-with-openssl-rand-base64-32
NEXTAUTH_URL=http://localhost:3020
# Node Environment
NODE_ENV=production
```
## Génération du secret NextAuth
```bash
openssl rand -base64 32
```
## Développement
Pour le développement, les variables sont définies directement dans `docker-compose.dev.yml`.

View File

@@ -29,6 +29,8 @@ services:
- MONGODB_URI=mongodb://admin:password123@mongodb:27017/stripstream?authSource=admin
- PNPM_HOME=/app/.pnpm-store
- WATCHPACK_POLLING=true
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- NEXTAUTH_URL=${NEXTAUTH_URL}
command: sh -c "pnpm config set store-dir /app/.pnpm-store && pnpm install --frozen-lockfile && pnpm dev"
mongodb:

View File

@@ -15,6 +15,8 @@ services:
environment:
- NODE_ENV=production
- MONGODB_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongodb:27017/stripstream?authSource=admin
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- NEXTAUTH_URL=${NEXTAUTH_URL}
depends_on:
- mongodb
networks:

View File

@@ -17,9 +17,9 @@
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-slot": "1.0.2",
"@radix-ui/react-toast": "1.1.5",
"@types/bcrypt": "^5.0.2",
"@types/bcryptjs": "^3.0.0",
"@types/mongoose": "5.11.97",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"framer-motion": "^10.18.0",
@@ -28,7 +28,7 @@
"lucide-react": "^0.487.0",
"mongoose": "8.1.0",
"next": "15.2.0",
"next-auth": "4.24.5",
"next-auth": "5.0.0-beta.29",
"next-themes": "0.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",

426
pnpm-lock.yaml generated
View File

@@ -26,15 +26,15 @@ importers:
'@radix-ui/react-toast':
specifier: 1.1.5
version: 1.1.5(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
'@types/bcrypt':
specifier: ^5.0.2
version: 5.0.2
'@types/bcryptjs':
specifier: ^3.0.0
version: 3.0.0
'@types/mongoose':
specifier: 5.11.97
version: 5.11.97
bcrypt:
specifier: ^5.1.1
version: 5.1.1
bcryptjs:
specifier: ^3.0.2
version: 3.0.2
class-variance-authority:
specifier: ^0.7.0
version: 0.7.1
@@ -60,8 +60,8 @@ importers:
specifier: 15.2.0
version: 15.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-auth:
specifier: 4.24.5
version: 4.24.5(next@15.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
specifier: 5.0.0-beta.29
version: 5.0.0-beta.29(next@15.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)
next-themes:
specifier: 0.2.1
version: 0.2.1(next@15.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -142,6 +142,20 @@ packages:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@auth/core@0.40.0':
resolution: {integrity: sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==}
peerDependencies:
'@simplewebauthn/browser': ^9.0.1
'@simplewebauthn/server': ^9.0.2
nodemailer: ^6.8.0
peerDependenciesMeta:
'@simplewebauthn/browser':
optional: true
'@simplewebauthn/server':
optional: true
nodemailer:
optional: true
'@babel/runtime@7.28.4':
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
@@ -445,10 +459,6 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@mapbox/node-pre-gyp@1.0.11':
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true
'@mongodb-js/saslprep@1.3.1':
resolution: {integrity: sha512-6nZrq5kfAz0POWyhljnbWQQJQ5uT8oE2ddX303q1uY0tWsivWKgBDXBBvuFPwOqRRalXJuVO9EjOdVtuhLX0zg==}
@@ -1065,8 +1075,9 @@ packages:
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
'@types/bcrypt@5.0.2':
resolution: {integrity: sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==}
'@types/bcryptjs@3.0.0':
resolution: {integrity: sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg==}
deprecated: This is a stub types definition. bcryptjs provides its own type definitions, so you do not need this installed.
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
@@ -1316,9 +1327,6 @@ packages:
cpu: [x64]
os: [win32]
abbrev@1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -1329,10 +1337,6 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1359,14 +1363,6 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
aproba@2.1.0:
resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==}
are-we-there-yet@2.0.0:
resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@@ -1450,9 +1446,9 @@ packages:
resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==}
hasBin: true
bcrypt@5.1.1:
resolution: {integrity: sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==}
engines: {node: '>= 10.0.0'}
bcryptjs@3.0.2:
resolution: {integrity: sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==}
hasBin: true
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
@@ -1512,10 +1508,6 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
chownr@2.0.0:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
@@ -1536,10 +1528,6 @@ packages:
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
@@ -1551,13 +1539,6 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
console-control-strings@1.1.0:
resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==}
cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -1613,9 +1594,6 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'}
delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -1910,10 +1888,6 @@ packages:
react-dom:
optional: true
fs-minipass@2.1.0:
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
engines: {node: '>= 8'}
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@@ -1932,11 +1906,6 @@ packages:
functions-have-names@1.2.3:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
gauge@3.0.2:
resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
engines: {node: '>=10'}
deprecated: This package is no longer supported.
generator-function@2.0.1:
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
engines: {node: '>= 0.4'}
@@ -2018,9 +1987,6 @@ packages:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
has-unicode@2.0.1:
resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -2028,10 +1994,6 @@ packages:
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
i18next-browser-languagedetector@8.2.0:
resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==}
@@ -2205,8 +2167,8 @@ packages:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
jose@4.15.9:
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
jose@6.1.0:
resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -2278,19 +2240,11 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-react@0.487.0:
resolution: {integrity: sha512-aKqhOQ+YmFnwq8dWgGjOuLc8V1R9/c/yOd+zDY4+ohsR2Jo05lSGc3WsstYPIzcTpeosN7LoCkLReUUITvaIvw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
@@ -2320,27 +2274,10 @@ packages:
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
minipass@3.3.6:
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
engines: {node: '>=8'}
minipass@5.0.0:
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
engines: {node: '>=8'}
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
minizlib@2.1.2:
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
engines: {node: '>= 8'}
mkdirp@1.0.4:
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
engines: {node: '>=10'}
hasBin: true
mongodb-connection-string-url@3.0.2:
resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==}
@@ -2405,14 +2342,19 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
next-auth@4.24.5:
resolution: {integrity: sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==}
next-auth@5.0.0-beta.29:
resolution: {integrity: sha512-Ukpnuk3NMc/LiOl32njZPySk7pABEzbjhMUFd5/n10I0ZNC7NCuVv8IY2JgbDek2t/PUOifQEoUiOOTLy4os5A==}
peerDependencies:
next: ^12.2.5 || ^13 || ^14
'@simplewebauthn/browser': ^9.0.1
'@simplewebauthn/server': ^9.0.2
next: ^14.0.0-0 || ^15.0.0-0
nodemailer: ^6.6.5
react: ^17.0.2 || ^18
react-dom: ^17.0.2 || ^18
react: ^18.2.0 || ^19.0.0-0
peerDependenciesMeta:
'@simplewebauthn/browser':
optional: true
'@simplewebauthn/server':
optional: true
nodemailer:
optional: true
@@ -2444,26 +2386,9 @@ packages:
sass:
optional: true
node-addon-api@5.1.0:
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-releases@2.0.23:
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
nopt@5.0.0:
resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==}
engines: {node: '>=6'}
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -2472,21 +2397,13 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
npmlog@5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
deprecated: This package is no longer supported.
oauth@0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
oauth4webapi@3.8.2:
resolution: {integrity: sha512-FzZZ+bht5X0FKe7Mwz3DAVAmlH1BV5blSak/lHMBKz0/EBMhX6B10GlQYI51+oRp8ObJaX0g6pXrAxZh5s8rjw==}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@2.2.0:
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
engines: {node: '>= 6'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
@@ -2519,16 +2436,9 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
oidc-token-hash@5.1.1:
resolution: {integrity: sha512-D7EmwxJV6DsEB6vOFLrBM2OzsVgQzgPWyHlV2OOAVj772n+WTXpudC9e9u5BVKQnYwaD30Ivhi9b+4UeBcGu9g==}
engines: {node: ^10.13.0 || >=12.0.0}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
openid-client@5.7.1:
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -2643,21 +2553,18 @@ packages:
resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==}
engines: {node: ^10 || ^12 || >=14}
preact-render-to-string@5.2.6:
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
preact-render-to-string@6.5.11:
resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==}
peerDependencies:
preact: '>=10'
preact@10.27.2:
resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==}
preact@10.24.3:
resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
pretty-format@3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
@@ -2746,10 +2653,6 @@ packages:
read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
@@ -2794,9 +2697,6 @@ packages:
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
engines: {node: '>=0.4'}
safe-buffer@5.2.1:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
safe-push-apply@1.0.0:
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
engines: {node: '>= 0.4'}
@@ -2817,9 +2717,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
set-blocking@2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -2867,9 +2764,6 @@ packages:
sift@16.0.1:
resolution: {integrity: sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==}
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
@@ -2930,9 +2824,6 @@ packages:
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
engines: {node: '>= 0.4'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -2988,10 +2879,6 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
tar@6.2.1:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
@@ -3010,9 +2897,6 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
@@ -3122,17 +3006,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -3141,9 +3018,6 @@ packages:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
engines: {node: '>= 0.4'}
@@ -3165,9 +3039,6 @@ packages:
engines: {node: '>= 8'}
hasBin: true
wide-align@1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}
word-wrap@1.2.5:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
@@ -3183,9 +3054,6 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
yaml@2.8.1:
resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==}
engines: {node: '>= 14.6'}
@@ -3202,6 +3070,14 @@ snapshots:
'@alloc/quick-lru@5.2.0': {}
'@auth/core@0.40.0':
dependencies:
'@panva/hkdf': 1.2.1
jose: 6.1.0
oauth4webapi: 3.8.2
preact: 10.24.3
preact-render-to-string: 6.5.11(preact@10.24.3)
'@babel/runtime@7.28.4': {}
'@emnapi/core@1.5.0':
@@ -3458,21 +3334,6 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@mapbox/node-pre-gyp@1.0.11':
dependencies:
detect-libc: 2.1.2
https-proxy-agent: 5.0.1
make-dir: 3.1.0
node-fetch: 2.7.0
nopt: 5.0.0
npmlog: 5.0.1
rimraf: 3.0.2
semver: 7.7.3
tar: 6.2.1
transitivePeerDependencies:
- encoding
- supports-color
'@mongodb-js/saslprep@1.3.1':
dependencies:
sparse-bitfield: 3.0.3
@@ -4039,9 +3900,9 @@ snapshots:
tslib: 2.8.1
optional: true
'@types/bcrypt@5.0.2':
'@types/bcryptjs@3.0.0':
dependencies:
'@types/node': 24.7.2
bcryptjs: 3.0.2
'@types/json-schema@7.0.15': {}
@@ -4317,20 +4178,12 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
abbrev@1.1.1: {}
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
acorn@8.15.0: {}
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -4355,13 +4208,6 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
aproba@2.1.0: {}
are-we-there-yet@2.0.0:
dependencies:
delegates: 1.0.0
readable-stream: 3.6.2
arg@5.0.2: {}
argparse@2.0.1: {}
@@ -4467,13 +4313,7 @@ snapshots:
baseline-browser-mapping@2.8.16: {}
bcrypt@5.1.1:
dependencies:
'@mapbox/node-pre-gyp': 1.0.11
node-addon-api: 5.1.0
transitivePeerDependencies:
- encoding
- supports-color
bcryptjs@3.0.2: {}
binary-extensions@2.3.0: {}
@@ -4544,8 +4384,6 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
chownr@2.0.0: {}
class-variance-authority@0.7.1:
dependencies:
clsx: 2.1.1
@@ -4565,8 +4403,6 @@ snapshots:
color-name: 1.1.4
simple-swizzle: 0.2.4
color-support@1.1.3: {}
color@4.2.3:
dependencies:
color-convert: 2.0.1
@@ -4576,10 +4412,6 @@ snapshots:
concat-map@0.0.1: {}
console-control-strings@1.1.0: {}
cookie@0.5.0: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -4632,8 +4464,6 @@ snapshots:
has-property-descriptors: 1.0.2
object-keys: 1.1.1
delegates@1.0.0: {}
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
@@ -5069,10 +4899,6 @@ snapshots:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
fs-minipass@2.1.0:
dependencies:
minipass: 3.3.6
fs.realpath@1.0.0: {}
fsevents@2.3.3:
@@ -5091,18 +4917,6 @@ snapshots:
functions-have-names@1.2.3: {}
gauge@3.0.2:
dependencies:
aproba: 2.1.0
color-support: 1.1.3
console-control-strings: 1.1.0
has-unicode: 2.0.1
object-assign: 4.1.1
signal-exit: 3.0.7
string-width: 4.2.3
strip-ansi: 6.0.1
wide-align: 1.1.5
generator-function@2.0.1: {}
get-intrinsic@1.3.0:
@@ -5201,8 +5015,6 @@ snapshots:
dependencies:
has-symbols: 1.1.0
has-unicode@2.0.1: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
@@ -5211,13 +5023,6 @@ snapshots:
dependencies:
void-elements: 3.1.0
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
i18next-browser-languagedetector@8.2.0:
dependencies:
'@babel/runtime': 7.28.4
@@ -5395,7 +5200,7 @@ snapshots:
jiti@1.21.7: {}
jose@4.15.9: {}
jose@6.1.0: {}
js-tokens@4.0.0: {}
@@ -5457,18 +5262,10 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
lucide-react@0.487.0(react@18.2.0):
dependencies:
react: 18.2.0
make-dir@3.1.0:
dependencies:
semver: 6.3.1
math-intrinsics@1.1.0: {}
memory-pager@1.5.0: {}
@@ -5494,21 +5291,8 @@ snapshots:
minimist@1.2.8: {}
minipass@3.3.6:
dependencies:
yallist: 4.0.0
minipass@5.0.0: {}
minipass@7.1.2: {}
minizlib@2.1.2:
dependencies:
minipass: 3.3.6
yallist: 4.0.0
mkdirp@1.0.4: {}
mongodb-connection-string-url@3.0.2:
dependencies:
'@types/whatwg-url': 11.0.5
@@ -5563,20 +5347,11 @@ snapshots:
natural-compare@1.4.0: {}
next-auth@4.24.5(next@15.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
next-auth@5.0.0-beta.29(next@15.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.28.4
'@panva/hkdf': 1.2.1
cookie: 0.5.0
jose: 4.15.9
'@auth/core': 0.40.0
next: 15.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.27.2
preact-render-to-string: 5.2.6(preact@10.27.2)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
uuid: 8.3.2
next-themes@0.2.1(next@15.2.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
@@ -5609,35 +5384,16 @@ snapshots:
- '@babel/core'
- babel-plugin-macros
node-addon-api@5.1.0: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-releases@2.0.23: {}
nopt@5.0.0:
dependencies:
abbrev: 1.1.1
normalize-path@3.0.0: {}
normalize-range@0.1.2: {}
npmlog@5.0.1:
dependencies:
are-we-there-yet: 2.0.0
console-control-strings: 1.1.0
gauge: 3.0.2
set-blocking: 2.0.0
oauth@0.9.15: {}
oauth4webapi@3.8.2: {}
object-assign@4.1.1: {}
object-hash@2.2.0: {}
object-hash@3.0.0: {}
object-inspect@1.13.4: {}
@@ -5680,19 +5436,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
oidc-token-hash@5.1.1: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
openid-client@5.7.1:
dependencies:
jose: 4.15.9
lru-cache: 6.0.0
object-hash: 2.2.0
oidc-token-hash: 5.1.1
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -5792,17 +5539,14 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
preact-render-to-string@5.2.6(preact@10.27.2):
preact-render-to-string@6.5.11(preact@10.24.3):
dependencies:
preact: 10.27.2
pretty-format: 3.8.0
preact: 10.24.3
preact@10.27.2: {}
preact@10.24.3: {}
prelude-ls@1.2.1: {}
pretty-format@3.8.0: {}
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
@@ -5882,12 +5626,6 @@ snapshots:
dependencies:
pify: 2.3.0
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
@@ -5946,8 +5684,6 @@ snapshots:
has-symbols: 1.1.0
isarray: 2.0.5
safe-buffer@5.2.1: {}
safe-push-apply@1.0.0:
dependencies:
es-errors: 1.3.0
@@ -5967,8 +5703,6 @@ snapshots:
semver@7.7.3: {}
set-blocking@2.0.0: {}
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -6080,8 +5814,6 @@ snapshots:
sift@16.0.1: {}
signal-exit@3.0.7: {}
signal-exit@4.1.0: {}
simple-swizzle@0.2.4:
@@ -6167,10 +5899,6 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@@ -6237,15 +5965,6 @@ snapshots:
transitivePeerDependencies:
- ts-node
tar@6.2.1:
dependencies:
chownr: 2.0.0
fs-minipass: 2.1.0
minipass: 5.0.0
minizlib: 2.1.2
mkdirp: 1.0.4
yallist: 4.0.0
text-table@0.2.0: {}
thenify-all@1.6.0:
@@ -6265,8 +5984,6 @@ snapshots:
dependencies:
is-number: 7.0.0
tr46@0.0.3: {}
tr46@5.1.1:
dependencies:
punycode: 2.3.1
@@ -6400,12 +6117,8 @@ snapshots:
util-deprecate@1.0.2: {}
uuid@8.3.2: {}
void-elements@3.1.0: {}
webidl-conversions@3.0.1: {}
webidl-conversions@7.0.0: {}
whatwg-url@14.2.0:
@@ -6413,11 +6126,6 @@ snapshots:
tr46: 5.1.1
webidl-conversions: 7.0.0
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which-boxed-primitive@1.1.1:
dependencies:
is-bigint: 1.1.0
@@ -6463,10 +6171,6 @@ snapshots:
dependencies:
isexe: 2.0.0
wide-align@1.1.5:
dependencies:
string-width: 4.2.3
word-wrap@1.2.5: {}
wrap-ansi@7.0.0:
@@ -6483,8 +6187,6 @@ snapshots:
wrappy@1.0.2: {}
yallist@4.0.0: {}
yaml@2.8.1: {}
yocto-queue@0.1.0: {}

View File

@@ -0,0 +1,3 @@
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;

View File

@@ -1,44 +0,0 @@
import { NextResponse } from "next/server";
import { AuthServerService } from "@/lib/services/auth-server.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { AppError } from "@/utils/errors";
import type { UserData } from "@/lib/services/auth-server.service";
import { getErrorMessage } from "@/utils/errors";
import type { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
try {
const { email, password, remember } = await request.json();
try {
const userData: UserData = await AuthServerService.loginUser(email, password);
await AuthServerService.setUserCookie(userData, remember);
return NextResponse.json({
message: "✅ Connexion réussie",
user: userData,
});
} catch (error) {
if (error instanceof AppError) {
return NextResponse.json(
{
error,
},
{ status: 401 }
);
}
throw error;
}
} catch (error) {
console.error("Erreur lors de la connexion:", error);
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.INVALID_CREDENTIALS,
name: "Invalid credentials",
message: getErrorMessage(ERROR_CODES.AUTH.INVALID_CREDENTIALS),
} as AppError,
},
{ status: 500 }
);
}
}

View File

@@ -1,27 +0,0 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";
import { ERROR_CODES } from "@/constants/errorCodes";
import { getErrorMessage } from "@/utils/errors";
import type { AppErrorType } from "@/types/global";
export async function POST() {
try {
// Supprimer le cookie
const cookieStore = await cookies();
cookieStore.delete("stripUser");
return NextResponse.json({ message: "👋 Déconnexion réussie" });
} catch (error) {
console.error("Erreur lors de la déconnexion:", error);
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.LOGOUT_ERROR,
name: "Logout error",
message: getErrorMessage(ERROR_CODES.AUTH.LOGOUT_ERROR),
} as AppErrorType,
},
{ status: 500 }
);
}
}

View File

@@ -1,48 +1,52 @@
import { NextResponse } from "next/server";
import type { UserData } from "@/lib/services/auth-server.service";
import { NextRequest, NextResponse } from "next/server";
import { AuthServerService } from "@/lib/services/auth-server.service";
import { ERROR_CODES } from "@/constants/errorCodes";
import { ERROR_MESSAGES } from "@/constants/errorMessages";
import { AppError } from "@/utils/errors";
import { getErrorMessage } from "@/utils/errors";
import type { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json();
try {
const userData: UserData = await AuthServerService.createUser(email, password);
await AuthServerService.setUserCookie(userData);
return NextResponse.json({
message: "✅ Inscription réussie",
user: userData,
});
} catch (error) {
if (error instanceof AppError) {
const status =
error.code === ERROR_CODES.AUTH.EMAIL_EXISTS ||
error.code === ERROR_CODES.AUTH.PASSWORD_NOT_STRONG
? 400
: 500;
return NextResponse.json(
{
error,
},
{ status }
);
}
throw error;
if (!email || !password) {
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.INVALID_USER_DATA,
name: "Invalid user data",
message: ERROR_MESSAGES[ERROR_CODES.AUTH.INVALID_USER_DATA],
} as AppError,
},
{ status: 400 }
);
}
const userData = await AuthServerService.registerUser(email, password);
return NextResponse.json({ success: true, user: userData });
} catch (error) {
console.error("Erreur lors de l'inscription:", error);
console.error("Registration error:", error);
if (error instanceof AppError) {
return NextResponse.json(
{
error: {
code: error.code,
name: error.name,
message: error.message,
} as AppError,
},
{ status: 400 }
);
}
return NextResponse.json(
{
error: {
code: ERROR_CODES.AUTH.INVALID_USER_DATA,
name: "Invalid user data",
message: getErrorMessage(ERROR_CODES.AUTH.INVALID_USER_DATA),
},
code: ERROR_CODES.AUTH.REGISTRATION_FAILED,
name: "Registration failed",
message: ERROR_MESSAGES[ERROR_CODES.AUTH.REGISTRATION_FAILED],
} as AppError,
},
{ status: 500 }
);

View File

@@ -6,6 +6,7 @@ import ClientLayout from "@/components/layout/ClientLayout";
import { PreferencesService } from "@/lib/services/preferences.service";
import { PreferencesProvider } from "@/contexts/PreferencesContext";
import { I18nProvider } from "@/components/providers/I18nProvider";
import { AuthProvider } from "@/components/providers/AuthProvider";
import "@/i18n/i18n"; // Import i18next configuration
import { cookies } from "next/headers";
import { defaultPreferences } from "@/types/preferences";
@@ -158,13 +159,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<body
className={cn("min-h-screen bg-background font-sans antialiased h-full", inter.className)}
>
<I18nProvider locale={locale}>
<PreferencesProvider initialPreferences={preferences}>
<ClientLayout initialLibraries={libraries} initialFavorites={favorites}>
{children}
</ClientLayout>
</PreferencesProvider>
</I18nProvider>
<AuthProvider>
<I18nProvider locale={locale}>
<PreferencesProvider initialPreferences={preferences}>
<ClientLayout initialLibraries={libraries} initialFavorites={favorites}>
{children}
</ClientLayout>
</PreferencesProvider>
</I18nProvider>
</AuthProvider>
</body>
</html>
);

View File

@@ -2,9 +2,7 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { authService } from "@/lib/services/auth.service";
import type { AppErrorType } from "@/types/global";
import { ErrorMessage } from "@/components/ui/ErrorMessage";
import { signIn } from "next-auth/react";
import { useTranslate } from "@/hooks/useTranslate";
interface LoginFormProps {
@@ -14,7 +12,7 @@ interface LoginFormProps {
export function LoginForm({ from }: LoginFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<AppErrorType | null>(null);
const [error, setError] = useState<string | null>(null);
const { t } = useTranslate();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
@@ -28,11 +26,21 @@ export function LoginForm({ from }: LoginFormProps) {
const remember = formData.get("remember") === "on";
try {
await authService.login(email, password, remember);
router.push(from || "/");
router.refresh();
} catch (error) {
setError(error as AppErrorType);
const result = await signIn("credentials", {
email,
password,
remember,
redirect: false,
});
if (result?.error) {
setError("Email ou mot de passe incorrect");
} else {
router.push(from || "/");
router.refresh();
}
} catch (_error) {
setError("Une erreur est survenue lors de la connexion : " + _error);
} finally {
setIsLoading(false);
}
@@ -89,7 +97,11 @@ export function LoginForm({ from }: LoginFormProps) {
{t("login.form.remember")}
</label>
</div>
{error && <ErrorMessage errorCode={error.code} variant="form" />}
{error && (
<div className="text-red-600 text-sm">
{error}
</div>
)}
<button
type="submit"
disabled={isLoading}

View File

@@ -2,18 +2,16 @@
import { useState } from "react";
import { useRouter } from "next/navigation";
import { authService } from "@/lib/services/auth.service";
import type { AppErrorType } from "@/types/global";
import { ERROR_CODES } from "@/constants/errorCodes";
import { signIn } from "next-auth/react";
import { ErrorMessage } from "@/components/ui/ErrorMessage";
import { useTranslate } from "@/hooks/useTranslate";
import { getErrorMessage } from "@/utils/errors";
import type { AppErrorType } from "@/types/global";
interface RegisterFormProps {
from?: string;
}
export function RegisterForm({ from }: RegisterFormProps) {
export function RegisterForm({ from: _from }: RegisterFormProps) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<AppErrorType | null>(null);
@@ -31,20 +29,57 @@ export function RegisterForm({ from }: RegisterFormProps) {
if (password !== confirmPassword) {
setError({
code: ERROR_CODES.AUTH.PASSWORD_MISMATCH,
code: "AUTH_PASSWORD_MISMATCH",
name: "Password mismatch",
message: getErrorMessage(ERROR_CODES.AUTH.PASSWORD_MISMATCH),
message: "Les mots de passe ne correspondent pas",
});
setIsLoading(false);
return;
}
try {
await authService.register(email, password);
router.push(from || "/");
router.refresh();
} catch (error) {
setError(error as AppErrorType);
// Étape 1: Inscription via l'API
const response = await fetch("/api/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const data = await response.json();
setError(data.error || {
code: "AUTH_REGISTRATION_FAILED",
name: "Registration failed",
message: "Erreur lors de l'inscription",
});
return;
}
// Étape 2: Connexion automatique via NextAuth
const signInResult = await signIn("credentials", {
email,
password,
redirect: false,
});
if (signInResult?.error) {
setError({
code: "AUTH_INVALID_CREDENTIALS",
name: "Login failed",
message: "Inscription réussie mais erreur lors de la connexion automatique",
});
} else {
router.push("/");
router.refresh();
}
} catch {
setError({
code: "AUTH_REGISTRATION_FAILED",
name: "Registration failed",
message: "Une erreur est survenue lors de l'inscription",
});
} finally {
setIsLoading(false);
}

View File

@@ -3,7 +3,7 @@
import { Home, Library, Settings, LogOut, RefreshCw, Star, Download } from "lucide-react";
import { usePathname, useRouter } from "next/navigation";
import { cn } from "@/lib/utils";
import { authService } from "@/lib/services/auth.service";
import { signOut } from "next-auth/react";
import { useEffect, useState, useCallback } from "react";
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
import { usePreferences } from "@/contexts/PreferencesContext";
@@ -118,19 +118,15 @@ export function Sidebar({ isOpen, onClose, initialLibraries, initialFavorites }:
const handleLogout = async () => {
try {
await authService.logout();
await signOut({ callbackUrl: "/login" });
setLibraries([]);
setFavorites([]);
onClose();
router.push("/login");
} catch (error) {
console.error("Erreur lors de la déconnexion:", error);
toast({
title: "Erreur",
description:
error instanceof AppError
? error.message
: getErrorMessage(ERROR_CODES.AUTH.LOGOUT_ERROR),
description: "Une erreur est survenue lors de la déconnexion",
variant: "destructive",
});
}

View File

@@ -3,7 +3,7 @@
import { SeriesGrid } from "./SeriesGrid";
import { Pagination } from "@/components/ui/Pagination";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import type { KomgaSeries } from "@/types/komga";
@@ -27,7 +27,7 @@ export function PaginatedSeriesGrid({
series,
currentPage,
totalPages,
totalElements,
totalElements: _totalElements,
defaultShowOnlyUnread,
showOnlyUnread: initialShowOnlyUnread,
}: PaginatedSeriesGridProps) {
@@ -36,10 +36,10 @@ export function PaginatedSeriesGrid({
const searchParams = useSearchParams();
const [isChangingPage, setIsChangingPage] = useState(false);
const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread);
const { isCompact, itemsPerPage } = useDisplayPreferences();
const { isCompact, itemsPerPage: _itemsPerPage } = useDisplayPreferences();
const { t } = useTranslate();
const updateUrlParams = async (
const updateUrlParams = useCallback(async (
updates: Record<string, string | null>,
replace: boolean = false
) => {
@@ -59,7 +59,7 @@ export function PaginatedSeriesGrid({
} else {
await router.push(`${pathname}?${params.toString()}`);
}
};
}, [router, pathname, searchParams]);
// Reset loading state when series change
useEffect(() => {
@@ -76,7 +76,7 @@ export function PaginatedSeriesGrid({
if (defaultShowOnlyUnread && !searchParams.has("unread")) {
updateUrlParams({ page: "1", unread: "true" }, true);
}
}, [defaultShowOnlyUnread, pathname, router, searchParams]);
}, [defaultShowOnlyUnread, pathname, router, searchParams, updateUrlParams]);
const handlePageChange = async (page: number) => {
await updateUrlParams({ page: page.toString() });

View File

@@ -0,0 +1,12 @@
"use client";
import { SessionProvider } from "next-auth/react";
import { ReactNode } from "react";
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
return <SessionProvider>{children}</SessionProvider>;
}

View File

@@ -1,6 +1,5 @@
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import { cn } from "@/lib/utils";
import { useState } from "react";
interface ZoomablePageProps {
pageUrl: string | null;
@@ -23,10 +22,7 @@ export const ZoomablePage = ({
order = "first",
onZoomChange,
}: ZoomablePageProps) => {
const [currentScale, setCurrentScale] = useState(1);
const handleTransform = (ref: any, state: { scale: number; positionX: number; positionY: number }) => {
setCurrentScale(state.scale);
onZoomChange?.(state.scale > 1.1);
};
return (

View File

@@ -3,7 +3,7 @@
import { BookGrid } from "./BookGrid";
import { Pagination } from "@/components/ui/Pagination";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import type { KomgaBook } from "@/types/komga";
@@ -38,7 +38,7 @@ export function PaginatedBookGrid({
const { isCompact, itemsPerPage } = useDisplayPreferences();
const { t } = useTranslate();
const updateUrlParams = async (
const updateUrlParams = useCallback(async (
updates: Record<string, string | null>,
replace: boolean = false
) => {
@@ -58,7 +58,7 @@ export function PaginatedBookGrid({
} else {
await router.push(`${pathname}?${params.toString()}`);
}
};
}, [router, pathname, searchParams]);
// Reset loading state when books change
useEffect(() => {
@@ -75,7 +75,7 @@ export function PaginatedBookGrid({
if (defaultShowOnlyUnread && !searchParams.has("unread")) {
updateUrlParams({ page: "1", unread: "true" }, true);
}
}, [defaultShowOnlyUnread, pathname, router, searchParams]);
}, [defaultShowOnlyUnread, pathname, router, searchParams, updateUrlParams]);
const handlePageChange = async (page: number) => {
await updateUrlParams({ page: page.toString() });

View File

@@ -13,6 +13,7 @@ export const ERROR_CODES = {
EMAIL_EXISTS: "AUTH_EMAIL_EXISTS",
INVALID_USER_DATA: "AUTH_INVALID_USER_DATA",
LOGOUT_ERROR: "AUTH_LOGOUT_ERROR",
REGISTRATION_FAILED: "AUTH_REGISTRATION_FAILED",
},
KOMGA: {
MISSING_CONFIG: "KOMGA_MISSING_CONFIG",

View File

@@ -19,6 +19,7 @@ export const ERROR_MESSAGES: Record<string, string> = {
[ERROR_CODES.AUTH.EMAIL_EXISTS]: "📧 This email is already in use",
[ERROR_CODES.AUTH.INVALID_USER_DATA]: "👤 Invalid user data",
[ERROR_CODES.AUTH.LOGOUT_ERROR]: "🚪 Error during logout",
[ERROR_CODES.AUTH.REGISTRATION_FAILED]: "❌ Registration failed",
// Komga
[ERROR_CODES.KOMGA.MISSING_CONFIG]: "⚙️ Komga configuration not found",

17
src/lib/auth-utils.ts Normal file
View File

@@ -0,0 +1,17 @@
import { auth } from "@/lib/auth";
import type { UserData } from "@/lib/services/auth-server.service";
export async function getCurrentUser(): Promise<UserData | null> {
const session = await auth();
if (!session?.user) {
return null;
}
return {
id: session.user.id,
email: session.user.email,
roles: session.user.roles,
authenticated: true,
};
}

61
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,61 @@
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { AuthServerService } from "@/lib/services/auth-server.service";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
remember: { label: "Remember me", type: "checkbox" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
try {
const userData = await AuthServerService.loginUser(
credentials.email as string,
credentials.password as string
);
return {
id: userData.id,
email: userData.email,
roles: userData.roles,
};
} catch (error) {
console.error("Auth error:", error);
return null;
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
// Convertir le tableau en string pour éviter les problèmes de clonage
token.roles = JSON.stringify(user.roles);
}
return token;
},
async session({ session, token }) {
if (token) {
session.user.id = token.sub!;
// Reconvertir la string en tableau
session.user.roles = JSON.parse(token.roles as string);
}
return session;
},
},
pages: {
signIn: "/login",
},
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
});

View File

@@ -10,7 +10,14 @@ export function withPageTiming(pageName: string, Component: PageComponent) {
// Ensure params is awaited before using it
const params = props.params ? await Promise.resolve(props.params) : {};
await DebugService.logPageRender(pageName + JSON.stringify(params), duration);
// Only log if debug is enabled and user is authenticated
try {
await DebugService.logPageRender(pageName + JSON.stringify(params), duration);
} catch {
// Silently fail if user is not authenticated or debug is disabled
// This prevents errors on public pages like /login
}
return result;
};

View File

@@ -0,0 +1,26 @@
import { NextRequest } from "next/server";
import { getToken } from "next-auth/jwt";
export async function getAuthSession(request: NextRequest) {
try {
const token = await getToken({
req: request,
secret: process.env.NEXTAUTH_SECRET
});
if (!token) {
return null;
}
return {
user: {
id: token.sub!,
email: token.email!,
roles: JSON.parse(token.roles as string),
}
};
} catch (error) {
console.error("Auth error in middleware:", error);
return null;
}
}

View File

@@ -1,7 +1,6 @@
import { cookies } from "next/headers";
import connectDB from "@/lib/mongodb";
import { UserModel } from "@/lib/models/user.model";
import bcrypt from "bcrypt";
import bcrypt from "bcryptjs";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
@@ -15,7 +14,7 @@ export interface UserData {
export class AuthServerService {
private static readonly SALT_ROUNDS = 10;
static async createUser(email: string, password: string): Promise<UserData> {
static async registerUser(email: string, password: string): Promise<UserData> {
await connectDB();
//check if password is strong
@@ -67,37 +66,6 @@ export class AuthServerService {
return true;
}
static async setUserCookie(userData: UserData, remember: boolean = false): Promise<void> {
// Encode user data in base64
const encodedUserData = Buffer.from(JSON.stringify(userData)).toString("base64");
// Set cookie with user data
const cookieStore = await cookies();
cookieStore.set("stripUser", encodedUserData, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: remember ? 30 * 24 * 60 * 60 : 24 * 60 * 60, // 30 days if remember, 24 hours otherwise
});
}
static async getCurrentUser(): Promise<UserData | null> {
const cookieStore = await cookies();
const userCookie = cookieStore.get("stripUser");
if (!userCookie) {
return null;
}
try {
return JSON.parse(atob(userCookie.value));
} catch (error) {
console.error("Error while getting user from cookie:", error);
return null;
}
}
static async loginUser(email: string, password: string): Promise<UserData> {
await connectDB();

View File

@@ -1,105 +0,0 @@
"use client";
import type { AppErrorType } from "@/types/global";
import { ERROR_CODES } from "@/constants/errorCodes";
class AuthService {
private static instance: AuthService;
// Constructeur privé pour le pattern Singleton
private constructor() {
// Pas d'initialisation nécessaire
}
public static getInstance(): AuthService {
if (!AuthService.instance) {
AuthService.instance = new AuthService();
}
return AuthService.instance;
}
/**
* Authentifie un utilisateur
*/
async login(email: string, password: string, remember: boolean = false): Promise<void> {
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password, remember }),
});
if (!response.ok) {
const data = await response.json();
throw data.error;
}
} catch (error) {
if ((error as AppErrorType).code) {
throw error;
}
throw {
code: ERROR_CODES.AUTH.INVALID_CREDENTIALS,
name: "Invalid credentials",
message: "The email or password is incorrect",
} as AppErrorType;
}
}
/**
* Crée un nouvel utilisateur
*/
async register(email: string, password: string): Promise<void> {
try {
const response = await fetch("/api/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const data = await response.json();
throw data.error;
}
} catch (error) {
if ((error as AppErrorType).code) {
throw error;
}
throw {
code: ERROR_CODES.AUTH.INVALID_USER_DATA,
name: "Invalid user data",
message: "The email or password is incorrect",
} as AppErrorType;
}
}
/**
* Déconnecte l'utilisateur
*/
async logout(): Promise<void> {
try {
const response = await fetch("/api/auth/logout", {
method: "POST",
});
if (!response.ok) {
const data = await response.json();
throw data.error;
}
} catch (error) {
if ((error as AppErrorType).code) {
throw error;
}
throw {
code: ERROR_CODES.AUTH.LOGOUT_ERROR,
name: "Logout error",
message: "The logout failed",
} as AppErrorType;
}
}
}
export const authService = AuthService.getInstance();

View File

@@ -2,14 +2,14 @@ import connectDB from "@/lib/mongodb";
import { KomgaConfig as KomgaConfigModel } from "@/lib/models/config.model";
import { TTLConfig as TTLConfigModel } from "@/lib/models/ttl-config.model";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { User, KomgaConfigData, TTLConfigData, KomgaConfig, TTLConfig } from "@/types/komga";
export class ConfigDBService {
private static async getCurrentUser(): Promise<User> {
const user: User | null = await AuthServerService.getCurrentUser();
const user: User | null = await getCurrentUser();
if (!user) {
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}

View File

@@ -1,8 +1,8 @@
import fs from "fs/promises";
import path from "path";
import type { CacheType } from "./base-api.service";
import { AuthServerService } from "./auth-server.service";
import { PreferencesService } from "./preferences.service";
import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
@@ -28,7 +28,7 @@ export class DebugService {
private static writeQueues = new Map<string, Promise<void>>();
private static async getCurrentUserId(): Promise<string> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}
@@ -49,7 +49,7 @@ export class DebugService {
}
private static async isDebugEnabled(): Promise<boolean> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
return false;
}

View File

@@ -1,7 +1,7 @@
import connectDB from "@/lib/mongodb";
import { FavoriteModel } from "@/lib/models/favorite.model";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { User } from "@/types/komga";
@@ -17,7 +17,7 @@ export class FavoriteService {
}
private static async getCurrentUser(): Promise<User> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}

View File

@@ -1,5 +1,5 @@
import { PreferencesModel } from "@/lib/models/preferences.model";
import { AuthServerService } from "./auth-server.service";
import { getCurrentUser } from "../auth-utils";
import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
import type { UserPreferences } from "@/types/preferences";
@@ -9,7 +9,7 @@ import connectDB from "@/lib/mongodb";
export class PreferencesService {
static async getCurrentUser(): Promise<User> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new AppError(ERROR_CODES.AUTH.UNAUTHENTICATED);
}

View File

@@ -33,6 +33,7 @@ class RequestMonitor {
} else if (count >= this.thresholds.high) {
console.warn(`[REQUEST-MONITOR] ⚠️ HIGH concurrency: ${count} active requests`);
} else if (count >= this.thresholds.warning) {
// eslint-disable-next-line no-console
console.log(`[REQUEST-MONITOR] ⚡ Warning concurrency: ${count} active requests`);
}
}

View File

@@ -2,7 +2,7 @@ import fs from "fs";
import path from "path";
import { PreferencesService } from "./preferences.service";
import { DebugService } from "./debug.service";
import { AuthServerService } from "./auth-server.service";
import { getCurrentUser } from "../auth-utils";
export type CacheMode = "file" | "memory";
@@ -45,7 +45,7 @@ class ServerCacheService {
private async initializeCacheMode(): Promise<void> {
try {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
this.setCacheMode("memory");
return;
@@ -293,7 +293,7 @@ class ServerCacheService {
* Supprime une entrée du cache
*/
async delete(key: string): Promise<void> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
}
@@ -313,7 +313,7 @@ class ServerCacheService {
* Supprime toutes les entrées du cache qui commencent par un préfixe
*/
async deleteAll(prefix: string): Promise<void> {
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
}
@@ -390,7 +390,7 @@ class ServerCacheService {
type: keyof typeof ServerCacheService.DEFAULT_TTL = "DEFAULT"
): Promise<T> {
const startTime = performance.now();
const user = await AuthServerService.getCurrentUser();
const user = await getCurrentUser();
if (!user) {
throw new Error("Utilisateur non authentifié");
}

View File

@@ -1,52 +1,32 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { ERROR_CODES } from "./constants/errorCodes";
import type { UserData } from "./lib/services/auth-server.service";
import { getErrorMessage } from "./utils/errors";
import { NextResponse, NextRequest } from "next/server";
import { getAuthSession } from "@/lib/middleware-auth";
// Routes qui ne nécessitent pas d'authentification
const publicRoutes = ["/login", "/register", "/images"];
// Routes d'API qui ne nécessitent pas d'authentification
const publicApiRoutes = ["/api/auth/login", "/api/auth/register", "/api/komga/test"];
const publicApiRoutes = ["/api/auth/register", "/api/komga/test"];
// Langues supportées
const locales = ["fr", "en"];
const defaultLocale = "fr";
export function middleware(request: NextRequest) {
export default async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Gestion de la langue
let locale = request.cookies.get("NEXT_LOCALE")?.value;
let locale = request.headers.get("cookie")?.match(/NEXT_LOCALE=([^;]+)/)?.[1];
// Si pas de cookie de langue ou langue non supportée, on utilise la langue par défaut
if (!locale || !locales.includes(locale)) {
locale = defaultLocale;
// On s'assure que la réponse est bien une redirection si nécessaire
const response =
pathname === "/login"
? NextResponse.next()
: NextResponse.redirect(new URL("/login", request.url));
response.cookies.set("NEXT_LOCALE", locale, {
path: "/",
maxAge: 365 * 24 * 60 * 60, // 1 an
secure: true, // Ajout de secure pour HTTPS
sameSite: "lax", // Protection CSRF
});
return response;
}
// Gestion de l'authentification
const user = request.cookies.get("stripUser");
// Vérifier si c'est une route publique avant de gérer l'authentification
if (
publicRoutes.includes(pathname) ||
publicApiRoutes.includes(pathname) ||
pathname.startsWith("/api/auth/") ||
pathname.startsWith("/images/") ||
pathname.startsWith("/_next/") ||
pathname.startsWith("/fonts/")
@@ -54,14 +34,16 @@ export function middleware(request: NextRequest) {
return NextResponse.next();
}
// Pour toutes les routes protégées, vérifier la présence de l'utilisateur
if (!user?.value) {
// Vérifier l'authentification avec NextAuth v5
const session = await getAuthSession(request);
if (!session) {
if (pathname.startsWith("/api/")) {
return NextResponse.json(
{
error: {
code: ERROR_CODES.MIDDLEWARE.UNAUTHORIZED,
message: getErrorMessage(ERROR_CODES.MIDDLEWARE.UNAUTHORIZED),
code: "UNAUTHORIZED",
message: "Unauthorized access",
name: "Unauthorized",
},
},
@@ -70,35 +52,21 @@ export function middleware(request: NextRequest) {
}
const loginUrl = new URL("/login", request.url);
// loginUrl.searchParams.set("from", encodeURIComponent(pathname));
return NextResponse.redirect(loginUrl);
}
try {
const userData: UserData = JSON.parse(atob(user.value));
if (!userData || !userData.authenticated || !userData.id || !userData.email) {
throw new Error(getErrorMessage(ERROR_CODES.MIDDLEWARE.INVALID_SESSION));
}
} catch (error) {
console.error("Erreur de validation du cookie:", error);
if (pathname.startsWith("/api/")) {
return NextResponse.json(
{
error: {
code: ERROR_CODES.MIDDLEWARE.INVALID_TOKEN,
message: getErrorMessage(ERROR_CODES.MIDDLEWARE.INVALID_TOKEN),
name: "Invalid token",
},
},
{ status: 401 }
);
}
const loginUrl = new URL("/login", request.url);
// loginUrl.searchParams.set("from", pathname);
return NextResponse.redirect(loginUrl);
// Définir le cookie de langue si nécessaire
const response = NextResponse.next();
if (!request.headers.get("cookie")?.includes("NEXT_LOCALE") && locale) {
response.cookies.set("NEXT_LOCALE", locale, {
path: "/",
maxAge: 365 * 24 * 60 * 60, // 1 an
secure: true, // Ajout de secure pour HTTPS
sameSite: "lax", // Protection CSRF
});
}
return NextResponse.next();
return response;
}
// Configuration des routes à protéger
@@ -106,12 +74,12 @@ export const config = {
matcher: [
/*
* Match all request paths except:
* 1. /api/auth/* (authentication routes)
* 1. /api/auth/* (NextAuth routes)
* 2. /_next/* (Next.js internals)
* 3. /fonts/* (inside public directory)
* 4. /images/* (inside public directory)
* 5. Static files (manifest.json, favicon.ico, etc.)
*/
"/((?!api/auth/*|_next/static|_next/image|fonts|images|manifest.json|favicon.ico|sitemap.xml|sw.js|offline.html).*)",
"/((?!api/auth|_next/static|_next/image|fonts|images|manifest.json|favicon.ico|sitemap.xml|sw.js|offline.html).*)",
],
};
};

23
src/types/next-auth.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import type { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
email: string;
roles: string[];
} & DefaultSession["user"];
}
interface User {
id: string;
email: string;
roles: string[];
}
}
declare module "next-auth/jwt" {
interface JWT {
roles: string; // Stocké comme string JSON
}
}