Merge pull request #2 from julienfroidefond/refacto/confOnMongo
Refacto/conf on mongo
This commit is contained in:
@@ -2,7 +2,6 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
node_modules
|
node_modules
|
||||||
.next
|
.next
|
||||||
.env
|
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
|
|||||||
7
.env
Normal file
7
.env
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# MongoDB
|
||||||
|
MONGO_USER=admin
|
||||||
|
MONGO_PASSWORD=password
|
||||||
|
MONGODB_URI=mongodb://admin:password@mongodb.paniels.orb.local:27017/stripstream?authSource=admin
|
||||||
|
|
||||||
|
# Komga
|
||||||
|
NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com
|
||||||
7
.env.example
Normal file
7
.env.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# MongoDB
|
||||||
|
MONGO_USER=admin
|
||||||
|
MONGO_PASSWORD=password
|
||||||
|
MONGODB_URI=mongodb://admin:password@localhost:27017/stripstream?authSource=admin
|
||||||
|
|
||||||
|
# Komga
|
||||||
|
NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com
|
||||||
@@ -14,5 +14,20 @@ services:
|
|||||||
- /app/.next
|
- /app/.next
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com
|
- NEXT_PUBLIC_API_URL= ${NEXT_PUBLIC_API_URL}
|
||||||
command: npm run dev
|
command: npm run dev
|
||||||
|
|
||||||
|
mongodb:
|
||||||
|
image: mongo:latest
|
||||||
|
container_name: stripstream_mongodb
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER}
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
volumes:
|
||||||
|
- mongodb_data:/data/db
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodb_data:
|
||||||
|
|||||||
390
package-lock.json
generated
390
package-lock.json
generated
@@ -11,16 +11,19 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
"@radix-ui/react-toast": "^1.2.6",
|
||||||
|
"@types/mongoose": "^5.11.97",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.323.0",
|
"lucide-react": "^0.323.0",
|
||||||
|
"mongoose": "^8.10.0",
|
||||||
"next": "^14.1.0",
|
"next": "^14.1.0",
|
||||||
|
"next-auth": "^4.24.11",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.2",
|
||||||
"sonner": "^1.7.4",
|
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
@@ -52,6 +55,17 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.26.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz",
|
||||||
|
"integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
||||||
@@ -654,6 +668,14 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mongodb-js/saslprep": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==",
|
||||||
|
"dependencies": {
|
||||||
|
"sparse-bitfield": "^3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "14.2.23",
|
"version": "14.2.23",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz",
|
||||||
@@ -859,6 +881,14 @@
|
|||||||
"node": ">=12.4.0"
|
"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==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -1310,6 +1340,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.1",
|
||||||
|
"@radix-ui/react-context": "1.1.1",
|
||||||
|
"@radix-ui/react-direction": "1.1.0",
|
||||||
|
"@radix-ui/react-id": "1.1.0",
|
||||||
|
"@radix-ui/react-presence": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.0.2",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.2",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-toast": {
|
"node_modules/@radix-ui/react-toast": {
|
||||||
"version": "1.2.6",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
||||||
@@ -1516,6 +1575,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/mongoose": {
|
||||||
|
"version": "5.11.97",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz",
|
||||||
|
"integrity": "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==",
|
||||||
|
"deprecated": "Mongoose publishes its own types, so you do not need to install this package.",
|
||||||
|
"dependencies": {
|
||||||
|
"mongoose": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.17.17",
|
"version": "20.17.17",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz",
|
||||||
@@ -1560,6 +1628,19 @@
|
|||||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/webidl-conversions": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/whatwg-url": {
|
||||||
|
"version": "11.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
|
||||||
|
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/webidl-conversions": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
|
||||||
@@ -2237,6 +2318,14 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bson": {
|
||||||
|
"version": "6.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz",
|
||||||
|
"integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@@ -2472,6 +2561,14 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -2570,7 +2667,6 @@
|
|||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -4471,6 +4567,14 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "4.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||||
|
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -4540,6 +4644,14 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kareem": {
|
||||||
|
"version": "2.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
|
||||||
|
"integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -4662,6 +4774,11 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memory-pager": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
|
||||||
|
},
|
||||||
"node_modules/merge2": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -4716,11 +4833,104 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mongodb": {
|
||||||
|
"version": "6.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz",
|
||||||
|
"integrity": "sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@mongodb-js/saslprep": "^1.1.9",
|
||||||
|
"bson": "^6.10.1",
|
||||||
|
"mongodb-connection-string-url": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@aws-sdk/credential-providers": "^3.188.0",
|
||||||
|
"@mongodb-js/zstd": "^1.1.0 || ^2.0.0",
|
||||||
|
"gcp-metadata": "^5.2.0",
|
||||||
|
"kerberos": "^2.0.1",
|
||||||
|
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||||
|
"snappy": "^7.2.2",
|
||||||
|
"socks": "^2.7.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@aws-sdk/credential-providers": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@mongodb-js/zstd": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"gcp-metadata": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"kerberos": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"mongodb-client-encryption": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"snappy": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"socks": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mongodb-connection-string-url": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/whatwg-url": "^11.0.2",
|
||||||
|
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mongoose": {
|
||||||
|
"version": "8.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.10.0.tgz",
|
||||||
|
"integrity": "sha512-nLhk3Qrv6q/HpD2k1O7kbBqsq+/kmKpdv5KJ+LLhQlII3e1p/SSLoLP6jMuSiU6+iLK7zFw4T1niAk3mA3QVug==",
|
||||||
|
"dependencies": {
|
||||||
|
"bson": "^6.10.1",
|
||||||
|
"kareem": "2.6.3",
|
||||||
|
"mongodb": "~6.13.0",
|
||||||
|
"mpath": "0.9.0",
|
||||||
|
"mquery": "5.0.0",
|
||||||
|
"ms": "2.1.3",
|
||||||
|
"sift": "17.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mongoose"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mpath": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mquery": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "4.x"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mz": {
|
"node_modules/mz": {
|
||||||
@@ -4809,6 +5019,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-auth": {
|
||||||
|
"version": "4.24.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz",
|
||||||
|
"integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@panva/hkdf": "^1.0.2",
|
||||||
|
"cookie": "^0.7.0",
|
||||||
|
"jose": "^4.15.5",
|
||||||
|
"oauth": "^0.9.15",
|
||||||
|
"openid-client": "^5.4.0",
|
||||||
|
"preact": "^10.6.3",
|
||||||
|
"preact-render-to-string": "^5.1.19",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@auth/core": "0.34.2",
|
||||||
|
"next": "^12.2.5 || ^13 || ^14 || ^15",
|
||||||
|
"nodemailer": "^6.6.5",
|
||||||
|
"react": "^17.0.2 || ^18 || ^19",
|
||||||
|
"react-dom": "^17.0.2 || ^18 || ^19"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@auth/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next-themes": {
|
"node_modules/next-themes": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
|
||||||
@@ -4872,6 +5113,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth": {
|
||||||
|
"version": "0.9.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||||
|
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -5002,6 +5248,14 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oidc-token-hash": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || >=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/once": {
|
"node_modules/once": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
@@ -5012,6 +5266,39 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openid-client": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
||||||
|
"dependencies": {
|
||||||
|
"jose": "^4.15.9",
|
||||||
|
"lru-cache": "^6.0.0",
|
||||||
|
"object-hash": "^2.2.0",
|
||||||
|
"oidc-token-hash": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client/node_modules/lru-cache": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"yallist": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openid-client/node_modules/object-hash": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -5343,6 +5630,26 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.25.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.25.4.tgz",
|
||||||
|
"integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/preact-render-to-string": {
|
||||||
|
"version": "5.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
|
||||||
|
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
||||||
|
"dependencies": {
|
||||||
|
"pretty-format": "^3.8.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -5353,6 +5660,11 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "3.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||||
|
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew=="
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -5369,7 +5681,6 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -5540,6 +5851,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||||
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||||
|
},
|
||||||
"node_modules/regexp.prototype.flags": {
|
"node_modules/regexp.prototype.flags": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
|
||||||
@@ -5934,6 +6250,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sift": {
|
||||||
|
"version": "17.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
|
||||||
|
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="
|
||||||
|
},
|
||||||
"node_modules/signal-exit": {
|
"node_modules/signal-exit": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
@@ -5965,15 +6286,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sonner": {
|
|
||||||
"version": "1.7.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz",
|
|
||||||
"integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -5983,6 +6295,14 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sparse-bitfield": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"memory-pager": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable-hash": {
|
"node_modules/stable-hash": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz",
|
||||||
@@ -6399,6 +6719,17 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
||||||
@@ -6671,6 +7002,34 @@
|
|||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "14.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz",
|
||||||
|
"integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.0.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@@ -6885,6 +7244,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
|
||||||
|
|||||||
@@ -13,11 +13,15 @@
|
|||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"@radix-ui/react-toast": "^1.2.6",
|
"@radix-ui/react-toast": "^1.2.6",
|
||||||
|
"@types/mongoose": "^5.11.97",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.323.0",
|
"lucide-react": "^0.323.0",
|
||||||
|
"mongoose": "^8.10.0",
|
||||||
"next": "^14.1.0",
|
"next": "^14.1.0",
|
||||||
|
"next-auth": "^4.24.11",
|
||||||
"next-themes": "^0.4.4",
|
"next-themes": "^0.4.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
39
src/app/api/auth/login/route.ts
Normal file
39
src/app/api/auth/login/route.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { AuthServerService } from "@/lib/services/auth-server.service";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const { email, password } = await request.json();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userData = await AuthServerService.loginUser(email, password);
|
||||||
|
AuthServerService.setUserCookie(userData);
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Connexion réussie", user: userData });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === "INVALID_CREDENTIALS") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: "INVALID_CREDENTIALS",
|
||||||
|
message: "Email ou mot de passe incorrect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la connexion:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: "SERVER_ERROR",
|
||||||
|
message: "Une erreur est survenue lors de la connexion",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/app/api/auth/logout/route.ts
Normal file
9
src/app/api/auth/logout/route.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
// Supprimer le cookie
|
||||||
|
cookies().delete("stripUser");
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Déconnexion réussie" });
|
||||||
|
}
|
||||||
39
src/app/api/auth/register/route.ts
Normal file
39
src/app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { AuthServerService } from "@/lib/services/auth-server.service";
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const { email, password } = await request.json();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userData = await AuthServerService.createUser(email, password);
|
||||||
|
AuthServerService.setUserCookie(userData);
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Inscription réussie", user: userData });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === "EMAIL_EXISTS") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: "EMAIL_EXISTS",
|
||||||
|
message: "Cet email est déjà utilisé",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de l'inscription:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: {
|
||||||
|
code: "SERVER_ERROR",
|
||||||
|
message: "Une erreur est survenue lors de l'inscription",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,57 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import { ConfigDBService } from "@/lib/services/config-db.service";
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
const data = await request.json();
|
const data = await request.json();
|
||||||
console.log("Configuration Komga reçue:", data);
|
const mongoConfig = await ConfigDBService.saveConfig(data);
|
||||||
|
// Convertir le document Mongoose en objet simple
|
||||||
return NextResponse.json({ message: "Configuration reçue avec succès" }, { status: 200 });
|
const config = {
|
||||||
} catch (error) {
|
url: mongoConfig.url,
|
||||||
console.error("Erreur lors de la réception de la configuration:", error);
|
username: mongoConfig.username,
|
||||||
|
password: mongoConfig.password,
|
||||||
|
userId: mongoConfig.userId,
|
||||||
|
};
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Erreur lors de la réception de la configuration" },
|
{ message: "Configuration sauvegardée avec succès", config },
|
||||||
{ status: 400 }
|
{ status: 200 }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la sauvegarde de la configuration:", error);
|
||||||
|
if (error instanceof Error && error.message === "Utilisateur non authentifié") {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Erreur lors de la sauvegarde de la configuration" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const mongoConfig = await ConfigDBService.getConfig();
|
||||||
|
// Convertir le document Mongoose en objet simple
|
||||||
|
const config = {
|
||||||
|
url: mongoConfig.url,
|
||||||
|
username: mongoConfig.username,
|
||||||
|
password: mongoConfig.password,
|
||||||
|
userId: mongoConfig.userId,
|
||||||
|
};
|
||||||
|
return NextResponse.json(config, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération de la configuration:", error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message === "Utilisateur non authentifié") {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
|
if (error.message === "Configuration non trouvée") {
|
||||||
|
return NextResponse.json({ error: "Configuration non trouvée" }, { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Erreur lors de la récupération de la configuration" },
|
||||||
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/app/api/komga/favorites/route.ts
Normal file
37
src/app/api/komga/favorites/route.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { FavoriteService } from "@/lib/services/favorite.service";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const favoriteIds = await FavoriteService.getAllFavoriteIds();
|
||||||
|
return NextResponse.json(favoriteIds);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des favoris:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Erreur lors de la récupération des favoris" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const { seriesId } = await request.json();
|
||||||
|
await FavoriteService.addToFavorites(seriesId);
|
||||||
|
return NextResponse.json({ message: "Favori ajouté avec succès" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de l'ajout du favori:", error);
|
||||||
|
return NextResponse.json({ error: "Erreur lors de l'ajout du favori" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(request: Request) {
|
||||||
|
try {
|
||||||
|
const { seriesId } = await request.json();
|
||||||
|
await FavoriteService.removeFromFavorites(seriesId);
|
||||||
|
return NextResponse.json({ message: "Favori supprimé avec succès" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la suppression du favori:", error);
|
||||||
|
return NextResponse.json({ error: "Erreur lors de la suppression du favori" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/app/api/komga/ttl-config/route.ts
Normal file
47
src/app/api/komga/ttl-config/route.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { ConfigDBService } from "@/lib/services/config-db.service";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const config = await ConfigDBService.getTTLConfig();
|
||||||
|
return NextResponse.json(config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération de la configuration TTL:", error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message === "Utilisateur non authentifié") {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Erreur lors de la récupération de la configuration TTL" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const data = await request.json();
|
||||||
|
const config = await ConfigDBService.saveTTLConfig(data);
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "Configuration TTL sauvegardée avec succès",
|
||||||
|
config: {
|
||||||
|
defaultTTL: config.defaultTTL,
|
||||||
|
homeTTL: config.homeTTL,
|
||||||
|
librariesTTL: config.librariesTTL,
|
||||||
|
seriesTTL: config.seriesTTL,
|
||||||
|
booksTTL: config.booksTTL,
|
||||||
|
imagesTTL: config.imagesTTL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la sauvegarde de la configuration TTL:", error);
|
||||||
|
if (error instanceof Error && error.message === "Utilisateur non authentifié") {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Erreur lors de la sauvegarde de la configuration TTL" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { cookies } from "next/headers";
|
|
||||||
import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid";
|
import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid";
|
||||||
import { komgaConfigService } from "@/lib/services/komga-config.service";
|
|
||||||
import { LibraryService } from "@/lib/services/library.service";
|
import { LibraryService } from "@/lib/services/library.service";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
|
|||||||
75
src/app/login/LoginContent.tsx
Normal file
75
src/app/login/LoginContent.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LoginForm } from "@/components/auth/LoginForm";
|
||||||
|
import { RegisterForm } from "@/components/auth/RegisterForm";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
|
interface LoginContentProps {
|
||||||
|
searchParams: {
|
||||||
|
from?: string;
|
||||||
|
tab?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginContent({ searchParams }: LoginContentProps) {
|
||||||
|
const defaultTab = searchParams.tab || "login";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container relative min-h-screen flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
|
||||||
|
<div className="relative hidden h-full flex-col bg-slate-800/80 p-10 text-white lg:flex dark:border-r overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-40 transition-opacity duration-200 ease-in-out"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url('/images/login-bg.jpg')",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-slate-800/20 to-slate-800/70" />
|
||||||
|
<div className="relative z-20 flex items-center text-lg font-medium">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="mr-2 h-6 w-6"
|
||||||
|
>
|
||||||
|
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
||||||
|
</svg>
|
||||||
|
StripStream
|
||||||
|
</div>
|
||||||
|
<div className="relative z-20 mt-auto">
|
||||||
|
<blockquote className="space-y-2">
|
||||||
|
<p className="text-lg">
|
||||||
|
Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne
|
||||||
|
et fluide.
|
||||||
|
</p>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="lg:p-8">
|
||||||
|
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
|
||||||
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">Bienvenue sur StripStream</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Connectez-vous ou créez un compte pour commencer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Tabs defaultValue={defaultTab} className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="login">Connexion</TabsTrigger>
|
||||||
|
<TabsTrigger value="register">Inscription</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="login">
|
||||||
|
<LoginForm from={searchParams.from} />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="register">
|
||||||
|
<RegisterForm from={searchParams.from} />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,187 +1,15 @@
|
|||||||
"use client";
|
import { Metadata } from "next";
|
||||||
|
import { LoginContent } from "./LoginContent";
|
||||||
|
|
||||||
import { useState, Suspense } from "react";
|
export const metadata: Metadata = {
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
title: "Connexion",
|
||||||
import { authService } from "@/lib/services/auth.service";
|
description: "Connectez-vous à votre compte StripStream",
|
||||||
import { AuthError } from "@/types/auth";
|
};
|
||||||
|
|
||||||
function LoginForm() {
|
export default function LoginPage({
|
||||||
const router = useRouter();
|
searchParams,
|
||||||
const searchParams = useSearchParams();
|
}: {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
searchParams: { from?: string; tab?: string };
|
||||||
const [error, setError] = useState<AuthError | null>(null);
|
}) {
|
||||||
|
return <LoginContent searchParams={searchParams} />;
|
||||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const formData = new FormData(event.currentTarget);
|
|
||||||
const email = formData.get("email") as string;
|
|
||||||
const password = formData.get("password") as string;
|
|
||||||
const remember = formData.get("remember") === "on";
|
|
||||||
|
|
||||||
try {
|
|
||||||
await authService.login(email, password, remember);
|
|
||||||
const from = searchParams.get("from") || "/";
|
|
||||||
router.push(from);
|
|
||||||
} catch (error) {
|
|
||||||
setError(error as AuthError);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container relative min-h-[calc(100vh)] flex-col items-center justify-center grid lg:max-w-none lg:grid-cols-2 lg:px-0">
|
|
||||||
<div className="relative hidden h-full flex-col bg-slate-800/80 p-10 text-white lg:flex dark:border-r overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-40 transition-opacity duration-200 ease-in-out"
|
|
||||||
style={{
|
|
||||||
backgroundImage: "url('/images/login-bg.jpg')",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-slate-800/20 to-slate-800/70" />
|
|
||||||
<div className="relative z-20 flex items-center text-lg font-medium">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="mr-2 h-6 w-6"
|
|
||||||
>
|
|
||||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
|
||||||
</svg>
|
|
||||||
Stripstream
|
|
||||||
</div>
|
|
||||||
<div className="relative z-20 mt-auto">
|
|
||||||
<blockquote className="space-y-2">
|
|
||||||
<p className="text-lg">
|
|
||||||
Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne
|
|
||||||
et fluide.
|
|
||||||
</p>
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="lg:p-8">
|
|
||||||
<div className="mx-auto flex w-full flex-col justify-center space-y-8 sm:w-[350px]">
|
|
||||||
<div className="flex flex-col items-center space-y-4">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute -inset-1 bg-gradient-to-r from-primary to-indigo-600 rounded-full blur opacity-70"></div>
|
|
||||||
<div className="relative bg-background rounded-full p-4">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className="h-12 w-12 text-primary"
|
|
||||||
>
|
|
||||||
<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2 text-center">
|
|
||||||
<h1 className="text-4xl font-bold bg-gradient-to-r from-primary to-indigo-600 bg-clip-text text-transparent">
|
|
||||||
Stripstream
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">Votre bibliothèque numérique de BD</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center">
|
|
||||||
<span className="w-full border-t" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center text-xs uppercase">
|
|
||||||
<span className="bg-background px-2 text-muted-foreground"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col space-y-2 text-center">
|
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Connexion</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Connectez-vous pour accéder à votre bibliothèque
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label
|
|
||||||
htmlFor="email"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="email"
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
required
|
|
||||||
defaultValue="demo@stripstream.local"
|
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
Mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
required
|
|
||||||
defaultValue="demo123"
|
|
||||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
id="remember"
|
|
||||||
name="remember"
|
|
||||||
type="checkbox"
|
|
||||||
defaultChecked
|
|
||||||
className="h-4 w-4 rounded border border-input ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="remember"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
Se souvenir de moi
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
|
||||||
{error.message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={isLoading}
|
|
||||||
className="inline-flex w-full items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isLoading ? "Connexion en cours..." : "Se connecter"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
|
||||||
return (
|
|
||||||
<Suspense
|
|
||||||
fallback={<div className="flex items-center justify-center min-h-screen">Chargement...</div>}
|
|
||||||
>
|
|
||||||
<LoginForm />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { HomeContent } from "@/components/home/HomeContent";
|
import { HomeContent } from "@/components/home/HomeContent";
|
||||||
import { HomeService } from "@/lib/services/home.service";
|
import { HomeService } from "@/lib/services/home.service";
|
||||||
import { cookies } from "next/headers";
|
|
||||||
import { komgaConfigService } from "@/lib/services/komga-config.service";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { cookies } from "next/headers";
|
|
||||||
import { PaginatedBookGrid } from "@/components/series/PaginatedBookGrid";
|
import { PaginatedBookGrid } from "@/components/series/PaginatedBookGrid";
|
||||||
import { SeriesHeader } from "@/components/series/SeriesHeader";
|
import { SeriesHeader } from "@/components/series/SeriesHeader";
|
||||||
import { komgaConfigService } from "@/lib/services/komga-config.service";
|
|
||||||
import { SeriesService } from "@/lib/services/series.service";
|
import { SeriesService } from "@/lib/services/series.service";
|
||||||
import { KomgaSeries } from "@/types/komga";
|
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: { seriesId: string };
|
params: { seriesId: string };
|
||||||
|
|||||||
@@ -1,429 +1,28 @@
|
|||||||
"use client";
|
import { ConfigDBService } from "@/lib/services/config-db.service";
|
||||||
|
import { ClientSettings } from "@/components/settings/ClientSettings";
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
export default async function SettingsPage() {
|
||||||
import { Loader2, Network, Trash2 } from "lucide-react";
|
let config = null;
|
||||||
import { useRouter } from "next/navigation";
|
let ttlConfig = null;
|
||||||
import { storageService } from "@/lib/services/storage.service";
|
|
||||||
import { AuthError } from "@/types/auth";
|
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
|
||||||
import { komgaConfigService } from "@/lib/services/komga-config.service";
|
|
||||||
|
|
||||||
interface ErrorMessage {
|
try {
|
||||||
message: string;
|
// Récupérer la configuration Komga
|
||||||
}
|
const mongoConfig = await ConfigDBService.getConfig();
|
||||||
|
if (mongoConfig) {
|
||||||
export default function SettingsPage() {
|
config = {
|
||||||
const router = useRouter();
|
url: mongoConfig.url,
|
||||||
const { toast } = useToast();
|
username: mongoConfig.username,
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
password: mongoConfig.password,
|
||||||
const [isCacheClearing, setIsCacheClearing] = useState(false);
|
userId: mongoConfig.userId,
|
||||||
const [error, setError] = useState<AuthError | null>(null);
|
};
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
}
|
||||||
const [config, setConfig] = useState({
|
|
||||||
serverUrl: "",
|
// Récupérer la configuration TTL
|
||||||
username: "",
|
ttlConfig = await ConfigDBService.getTTLConfig();
|
||||||
password: "",
|
} catch (error) {
|
||||||
});
|
console.error("Erreur lors de la récupération de la configuration:", error);
|
||||||
const [ttlConfig, setTTLConfig] = useState({
|
// On ne fait rien si la config n'existe pas, on laissera le composant client gérer l'état initial
|
||||||
defaultTTL: 5,
|
}
|
||||||
homeTTL: 5,
|
|
||||||
librariesTTL: 1440,
|
return <ClientSettings initialConfig={config} initialTTLConfig={ttlConfig} />;
|
||||||
seriesTTL: 5,
|
|
||||||
booksTTL: 5,
|
|
||||||
imagesTTL: 1440,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Charger la configuration existante
|
|
||||||
const savedConfig = storageService.getCredentials();
|
|
||||||
if (savedConfig) {
|
|
||||||
setConfig({
|
|
||||||
serverUrl: savedConfig.serverUrl,
|
|
||||||
username: savedConfig.credentials?.username || "",
|
|
||||||
password: savedConfig.credentials?.password || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Charger la configuration des TTL
|
|
||||||
const savedTTLConfig = storageService.getTTLConfig();
|
|
||||||
if (savedTTLConfig) {
|
|
||||||
setTTLConfig(savedTTLConfig);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleClearCache = async () => {
|
|
||||||
setIsCacheClearing(true);
|
|
||||||
setError(null);
|
|
||||||
setSuccess(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/komga/cache/clear", {
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
throw new Error(data.error || "Erreur lors de la suppression du cache");
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Cache supprimé",
|
|
||||||
description: "Cache serveur supprimé avec succès",
|
|
||||||
});
|
|
||||||
router.refresh();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur:", error);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Erreur",
|
|
||||||
description: error instanceof Error ? error.message : "Une erreur est survenue",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsCacheClearing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTest = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
setError(null);
|
|
||||||
setSuccess(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch("/api/komga/test", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
serverUrl: config.serverUrl,
|
|
||||||
username: config.username,
|
|
||||||
password: config.password,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
throw new Error(data.error || "Erreur lors du test de connexion");
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Connexion réussie",
|
|
||||||
description: "La connexion au serveur Komga a été établie avec succès",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur:", error);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Erreur",
|
|
||||||
description: error instanceof Error ? error.message : "Une erreur est survenue",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = (event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setSuccess(null);
|
|
||||||
|
|
||||||
const formData = new FormData(event.currentTarget);
|
|
||||||
const serverUrl = formData.get("serverUrl") as string;
|
|
||||||
const username = formData.get("username") as string;
|
|
||||||
const password = formData.get("password") as string;
|
|
||||||
|
|
||||||
const newConfig = {
|
|
||||||
serverUrl: serverUrl.trim(),
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
};
|
|
||||||
|
|
||||||
const komgaConfig = {
|
|
||||||
serverUrl: newConfig.serverUrl,
|
|
||||||
credentials: {
|
|
||||||
username: newConfig.username,
|
|
||||||
password: newConfig.password,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
komgaConfigService.setConfig(komgaConfig, true);
|
|
||||||
|
|
||||||
fetch("/api/komga/config", {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(komgaConfig),
|
|
||||||
});
|
|
||||||
|
|
||||||
setConfig(newConfig);
|
|
||||||
|
|
||||||
// Émettre un événement pour notifier les autres composants
|
|
||||||
const configChangeEvent = new CustomEvent("komga-config-changed", { detail: komgaConfig });
|
|
||||||
window.dispatchEvent(configChangeEvent);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Configuration sauvegardée",
|
|
||||||
description: "La configuration a été sauvegardée avec succès",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { name, value } = event.target;
|
|
||||||
setConfig((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[name]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTTLChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { name, value } = event.target;
|
|
||||||
setTTLConfig((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[name]: parseInt(value || "0", 10),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveTTL = (event: React.FormEvent<HTMLFormElement>) => {
|
|
||||||
event.preventDefault();
|
|
||||||
setSuccess(null);
|
|
||||||
|
|
||||||
storageService.setTTLConfig(ttlConfig);
|
|
||||||
toast({
|
|
||||||
title: "Configuration TTL sauvegardée",
|
|
||||||
description: "La configuration des TTL a été sauvegardée avec succès",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container max-w-3xl mx-auto py-8 space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h1 className="text-3xl font-bold">Préférences</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Messages de succès/erreur */}
|
|
||||||
{error && (
|
|
||||||
<div className="rounded-md bg-destructive/15 p-4">
|
|
||||||
<p className="text-sm text-destructive">{error.message}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{success && (
|
|
||||||
<div className="rounded-md bg-green-500/15 p-4">
|
|
||||||
<p className="text-sm text-green-500">{success}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-6">
|
|
||||||
{/* Section Configuration Komga */}
|
|
||||||
<div className="rounded-lg border bg-card text-card-foreground shadow-sm">
|
|
||||||
<div className="p-5 space-y-4">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
|
||||||
<Network className="h-5 w-5" />
|
|
||||||
Configuration Komga
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
Configurez les informations de connexion à votre serveur Komga.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Formulaire de configuration */}
|
|
||||||
<form onSubmit={handleSave} className="space-y-4">
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="serverUrl" className="text-sm font-medium">
|
|
||||||
L'URL du serveur
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
id="serverUrl"
|
|
||||||
name="serverUrl"
|
|
||||||
required
|
|
||||||
value={config.serverUrl}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="username" className="text-sm font-medium">
|
|
||||||
L'adresse email de connexion
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
required
|
|
||||||
value={config.username}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="password" className="text-sm font-medium">
|
|
||||||
Mot de passe
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
name="password"
|
|
||||||
required
|
|
||||||
value={config.password}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-1 inline-flex items-center justify-center rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Sauvegarder
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleTest}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="flex-1 inline-flex items-center justify-center rounded-md bg-secondary px-3 py-2 text-sm font-medium text-secondary-foreground ring-offset-background transition-colors hover:bg-secondary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Test en cours...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Tester la connexion"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Section Configuration du Cache */}
|
|
||||||
<div className="rounded-lg border bg-card text-card-foreground shadow-sm">
|
|
||||||
<div className="p-5 space-y-4">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
|
||||||
<Trash2 className="h-5 w-5" />
|
|
||||||
Configuration du Cache
|
|
||||||
</h2>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
Gérez les paramètres de mise en cache des données.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Formulaire TTL */}
|
|
||||||
<form onSubmit={handleSaveTTL} className="space-y-4">
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="defaultTTL" className="text-sm font-medium">
|
|
||||||
TTL par défaut (minutes)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="defaultTTL"
|
|
||||||
name="defaultTTL"
|
|
||||||
min="1"
|
|
||||||
value={ttlConfig.defaultTTL}
|
|
||||||
onChange={handleTTLChange}
|
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="homeTTL" className="text-sm font-medium">
|
|
||||||
TTL page d'accueil
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="homeTTL"
|
|
||||||
name="homeTTL"
|
|
||||||
min="1"
|
|
||||||
value={ttlConfig.homeTTL}
|
|
||||||
onChange={handleTTLChange}
|
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="librariesTTL" className="text-sm font-medium">
|
|
||||||
TTL bibliothèques
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="librariesTTL"
|
|
||||||
name="librariesTTL"
|
|
||||||
min="1"
|
|
||||||
value={ttlConfig.librariesTTL}
|
|
||||||
onChange={handleTTLChange}
|
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="seriesTTL" className="text-sm font-medium">
|
|
||||||
TTL séries
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="seriesTTL"
|
|
||||||
name="seriesTTL"
|
|
||||||
min="1"
|
|
||||||
value={ttlConfig.seriesTTL}
|
|
||||||
onChange={handleTTLChange}
|
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="booksTTL" className="text-sm font-medium">
|
|
||||||
TTL tomes
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="booksTTL"
|
|
||||||
name="booksTTL"
|
|
||||||
min="1"
|
|
||||||
value={ttlConfig.booksTTL}
|
|
||||||
onChange={handleTTLChange}
|
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="imagesTTL" className="text-sm font-medium">
|
|
||||||
TTL images
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="imagesTTL"
|
|
||||||
name="imagesTTL"
|
|
||||||
min="1"
|
|
||||||
value={ttlConfig.imagesTTL}
|
|
||||||
onChange={handleTTLChange}
|
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-1 inline-flex items-center justify-center rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Sauvegarder les TTL
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleClearCache}
|
|
||||||
disabled={isCacheClearing}
|
|
||||||
className="flex-1 inline-flex items-center justify-center rounded-md bg-destructive px-3 py-2 text-sm font-medium text-destructive-foreground ring-offset-background transition-colors hover:bg-destructive/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isCacheClearing ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
||||||
Suppression...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Vider le cache"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
103
src/components/auth/LoginForm.tsx
Normal file
103
src/components/auth/LoginForm.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { authService } from "@/lib/services/auth.service";
|
||||||
|
import { AuthError } from "@/types/auth";
|
||||||
|
|
||||||
|
interface LoginFormProps {
|
||||||
|
from?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginForm({ from }: LoginFormProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<AuthError | null>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
const email = formData.get("email") as string;
|
||||||
|
const password = formData.get("password") as string;
|
||||||
|
const remember = formData.get("remember") === "on";
|
||||||
|
|
||||||
|
try {
|
||||||
|
await authService.login(email, password, remember);
|
||||||
|
router.push(from || "/");
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
setError(error as AuthError);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
required
|
||||||
|
defaultValue="demo@stripstream.local"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
defaultValue="demo123"
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
id="remember"
|
||||||
|
name="remember"
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked
|
||||||
|
className="h-4 w-4 rounded border border-input ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="remember"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Se souvenir de moi
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||||
|
{error.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="inline-flex w-full items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? "Connexion en cours..." : "Se connecter"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
src/components/auth/RegisterForm.tsx
Normal file
111
src/components/auth/RegisterForm.tsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { authService } from "@/lib/services/auth.service";
|
||||||
|
import { AuthError } from "@/types/auth";
|
||||||
|
|
||||||
|
interface RegisterFormProps {
|
||||||
|
from?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RegisterForm({ from }: RegisterFormProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<AuthError | null>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
const email = formData.get("email") as string;
|
||||||
|
const password = formData.get("password") as string;
|
||||||
|
const confirmPassword = formData.get("confirmPassword") as string;
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError({
|
||||||
|
code: "SERVER_ERROR",
|
||||||
|
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 AuthError);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
required
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label
|
||||||
|
htmlFor="confirmPassword"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Confirmer le mot de passe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
required
|
||||||
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||||
|
{error.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="inline-flex w-full items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? "Inscription en cours..." : "S'inscrire"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,10 +11,10 @@ interface HeroSectionProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HeroSection({ series }: HeroSectionProps) {
|
export function HeroSection({ series }: HeroSectionProps) {
|
||||||
console.log("HeroSection - Séries reçues:", {
|
// console.log("HeroSection - Séries reçues:", {
|
||||||
count: series?.length || 0,
|
// count: series?.length || 0,
|
||||||
firstSeries: series?.[0],
|
// firstSeries: series?.[0],
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-[500px] -mx-4 sm:-mx-8 lg:-mx-14 overflow-hidden">
|
<div className="relative h-[500px] -mx-4 sm:-mx-8 lg:-mx-14 overflow-hidden">
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ export function HomeContent({ data }: HomeContentProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Vérification des données pour le debug
|
// Vérification des données pour le debug
|
||||||
console.log("HomeContent - Données reçues:", {
|
// console.log("HomeContent - Données reçues:", {
|
||||||
ongoingCount: data.ongoing?.length || 0,
|
// ongoingCount: data.ongoing?.length || 0,
|
||||||
recentlyReadCount: data.recentlyRead?.length || 0,
|
// recentlyReadCount: data.recentlyRead?.length || 0,
|
||||||
onDeckCount: data.onDeck?.length || 0,
|
// onDeckCount: data.onDeck?.length || 0,
|
||||||
});
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container mx-auto px-4 py-8 space-y-12">
|
<main className="container mx-auto px-4 py-8 space-y-12">
|
||||||
|
|||||||
@@ -17,16 +17,6 @@ export default function ClientLayout({ children }: { children: React.ReactNode }
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
// Vérification de l'authentification
|
|
||||||
useEffect(() => {
|
|
||||||
const isPublicRoute = publicRoutes.includes(pathname);
|
|
||||||
const isAuthenticated = authService.isAuthenticated();
|
|
||||||
|
|
||||||
if (!isAuthenticated && !isPublicRoute) {
|
|
||||||
router.push(`/login?from=${encodeURIComponent(pathname)}`);
|
|
||||||
}
|
|
||||||
}, [pathname, router]);
|
|
||||||
|
|
||||||
const handleCloseSidebar = () => {
|
const handleCloseSidebar = () => {
|
||||||
setIsSidebarOpen(false);
|
setIsSidebarOpen(false);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { BookOpen, Home, Library, Settings, LogOut, RefreshCw, Star } from "lucide-react";
|
import { BookOpen, Home, Library, Settings, LogOut, RefreshCw, Star } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
@@ -5,8 +7,6 @@ import { cn } from "@/lib/utils";
|
|||||||
import { authService } from "@/lib/services/auth.service";
|
import { authService } from "@/lib/services/auth.service";
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
import { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||||
import { storageService } from "@/lib/services/storage.service";
|
|
||||||
import { FavoriteService } from "@/lib/services/favorite.service";
|
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -43,13 +43,20 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|||||||
const fetchFavorites = useCallback(async () => {
|
const fetchFavorites = useCallback(async () => {
|
||||||
setIsLoadingFavorites(true);
|
setIsLoadingFavorites(true);
|
||||||
try {
|
try {
|
||||||
const favoriteIds = FavoriteService.getAllFavoriteIds();
|
// Récupérer les IDs des favoris depuis l'API
|
||||||
|
const favoritesResponse = await fetch("/api/komga/favorites");
|
||||||
|
if (!favoritesResponse.ok) {
|
||||||
|
throw new Error("Erreur lors de la récupération des favoris");
|
||||||
|
}
|
||||||
|
const favoriteIds = await favoritesResponse.json();
|
||||||
|
|
||||||
if (favoriteIds.length === 0) {
|
if (favoriteIds.length === 0) {
|
||||||
setFavorites([]);
|
setFavorites([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = favoriteIds.map(async (id) => {
|
// Récupérer les détails des séries pour chaque ID
|
||||||
|
const promises = favoriteIds.map(async (id: string) => {
|
||||||
const response = await fetch(`/api/komga/series/${id}`);
|
const response = await fetch(`/api/komga/series/${id}`);
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
return response.json();
|
return response.json();
|
||||||
@@ -69,7 +76,7 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchLibraries();
|
fetchLibraries();
|
||||||
fetchFavorites();
|
fetchFavorites();
|
||||||
}, []); // Suppression de la dépendance pathname
|
}, [fetchLibraries, fetchFavorites]);
|
||||||
|
|
||||||
// Mettre à jour les favoris quand ils changent
|
// Mettre à jour les favoris quand ils changent
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -77,18 +84,10 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|||||||
fetchFavorites();
|
fetchFavorites();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Écouter les changements de favoris dans la même fenêtre
|
|
||||||
window.addEventListener("favoritesChanged", handleFavoritesChange);
|
window.addEventListener("favoritesChanged", handleFavoritesChange);
|
||||||
// Écouter les changements de favoris dans d'autres fenêtres
|
|
||||||
window.addEventListener("storage", (e) => {
|
|
||||||
if (e.key === "stripstream_favorites") {
|
|
||||||
fetchFavorites();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("favoritesChanged", handleFavoritesChange);
|
window.removeEventListener("favoritesChanged", handleFavoritesChange);
|
||||||
window.removeEventListener("storage", handleFavoritesChange);
|
|
||||||
};
|
};
|
||||||
}, [fetchFavorites]);
|
}, [fetchFavorites]);
|
||||||
|
|
||||||
@@ -99,7 +98,6 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
|||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
authService.logout();
|
authService.logout();
|
||||||
storageService.clearAll();
|
|
||||||
setLibraries([]);
|
setLibraries([]);
|
||||||
setFavorites([]);
|
setFavorites([]);
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
25
src/components/layout/SidebarWrapper.tsx
Normal file
25
src/components/layout/SidebarWrapper.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { FavoriteService } from "@/lib/services/favorite.service";
|
||||||
|
import { LibraryService } from "@/lib/services/library.service";
|
||||||
|
|
||||||
|
export async function SidebarWrapper() {
|
||||||
|
// Récupérer les favoris depuis le serveur
|
||||||
|
const favoriteIds = await FavoriteService.getAllFavoriteIds();
|
||||||
|
|
||||||
|
// Récupérer les détails des séries favorites
|
||||||
|
const favoritesPromises = favoriteIds.map(async (id) => {
|
||||||
|
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/series/${id}`, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) return null;
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Récupérer les bibliothèques
|
||||||
|
const libraries = await LibraryService.getLibraries();
|
||||||
|
|
||||||
|
const favorites = (await Promise.all(favoritesPromises)).filter(Boolean);
|
||||||
|
|
||||||
|
return { favorites, libraries };
|
||||||
|
}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { ImageOff, Book, BookOpen, BookMarked } from "lucide-react";
|
import { ImageOff, Book, BookOpen, BookMarked, Star, StarOff } from "lucide-react";
|
||||||
import { KomgaSeries } from "@/types/komga";
|
import { KomgaSeries } from "@/types/komga";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { FavoriteService } from "@/lib/services/favorite.service";
|
|
||||||
import { Star, StarOff } from "lucide-react";
|
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -56,13 +54,27 @@ export const SeriesHeader = ({ series, onSeriesUpdate }: SeriesHeaderProps) => {
|
|||||||
const [readingStatus, setReadingStatus] = useState<ReadingStatusInfo>(
|
const [readingStatus, setReadingStatus] = useState<ReadingStatusInfo>(
|
||||||
getReadingStatusInfo(series)
|
getReadingStatusInfo(series)
|
||||||
);
|
);
|
||||||
const [isFavorite, setIsFavorite] = useState(FavoriteService.isFavorite(series.id));
|
const [isFavorite, setIsFavorite] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
// Vérifier si la série est dans les favoris au chargement
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const checkFavorite = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/komga/favorites");
|
||||||
|
if (response.ok) {
|
||||||
|
const favoriteIds = await response.json();
|
||||||
|
setIsFavorite(favoriteIds.includes(series.id));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la vérification des favoris:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkFavorite();
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, [series.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setReadingStatus(getReadingStatusInfo(series));
|
setReadingStatus(getReadingStatusInfo(series));
|
||||||
@@ -82,18 +94,29 @@ export const SeriesHeader = ({ series, onSeriesUpdate }: SeriesHeaderProps) => {
|
|||||||
}
|
}
|
||||||
}, [series.metadata.language]);
|
}, [series.metadata.language]);
|
||||||
|
|
||||||
const handleToggleFavorite = () => {
|
const handleToggleFavorite = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
if (isFavorite) {
|
const response = await fetch("/api/komga/favorites", {
|
||||||
FavoriteService.removeFromFavorites(series.id);
|
method: isFavorite ? "DELETE" : "POST",
|
||||||
} else {
|
headers: {
|
||||||
FavoriteService.addToFavorites(series.id);
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ seriesId: series.id }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la modification des favoris");
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsFavorite(!isFavorite);
|
setIsFavorite(!isFavorite);
|
||||||
if (onSeriesUpdate) {
|
if (onSeriesUpdate) {
|
||||||
onSeriesUpdate({ ...series, favorite: !isFavorite });
|
onSeriesUpdate({ ...series, favorite: !isFavorite });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dispatch l'événement pour notifier les autres composants
|
||||||
|
window.dispatchEvent(new Event("favoritesChanged"));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: isFavorite ? "Retiré des favoris" : "Ajouté aux favoris",
|
title: isFavorite ? "Retiré des favoris" : "Ajouté aux favoris",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
|
|||||||
487
src/components/settings/ClientSettings.tsx
Normal file
487
src/components/settings/ClientSettings.tsx
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Loader2, Network, Trash2 } from "lucide-react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { AuthError } from "@/types/auth";
|
||||||
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
|
|
||||||
|
interface ErrorMessage {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KomgaConfig {
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TTLConfigData {
|
||||||
|
defaultTTL: number;
|
||||||
|
homeTTL: number;
|
||||||
|
librariesTTL: number;
|
||||||
|
seriesTTL: number;
|
||||||
|
booksTTL: number;
|
||||||
|
imagesTTL: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientSettingsProps {
|
||||||
|
initialConfig: KomgaConfig | null;
|
||||||
|
initialTTLConfig: TTLConfigData | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ClientSettings({ initialConfig, initialTTLConfig }: ClientSettingsProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isCacheClearing, setIsCacheClearing] = useState(false);
|
||||||
|
const [error, setError] = useState<AuthError | null>(null);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
const [config, setConfig] = useState({
|
||||||
|
serverUrl: initialConfig?.url || "",
|
||||||
|
username: initialConfig?.username || "",
|
||||||
|
password: initialConfig?.password || "",
|
||||||
|
});
|
||||||
|
const [ttlConfig, setTTLConfig] = useState<TTLConfigData>(
|
||||||
|
initialTTLConfig || {
|
||||||
|
defaultTTL: 5,
|
||||||
|
homeTTL: 5,
|
||||||
|
librariesTTL: 1440,
|
||||||
|
seriesTTL: 5,
|
||||||
|
booksTTL: 5,
|
||||||
|
imagesTTL: 1440,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearCache = async () => {
|
||||||
|
setIsCacheClearing(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/komga/cache/clear", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.error || "Erreur lors de la suppression du cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Cache supprimé",
|
||||||
|
description: "Cache serveur supprimé avec succès",
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur:", error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Erreur",
|
||||||
|
description: error instanceof Error ? error.message : "Une erreur est survenue",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsCacheClearing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTest = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/komga/test", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
serverUrl: config.serverUrl,
|
||||||
|
username: config.username,
|
||||||
|
password: config.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.error || "Erreur lors du test de connexion");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Connexion réussie",
|
||||||
|
description: "La connexion au serveur Komga a été établie avec succès",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur:", error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Erreur",
|
||||||
|
description: error instanceof Error ? error.message : "Une erreur est survenue",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setSuccess(null);
|
||||||
|
setError(null);
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
const formData = new FormData(event.currentTarget);
|
||||||
|
const serverUrl = formData.get("serverUrl") as string;
|
||||||
|
const username = formData.get("username") as string;
|
||||||
|
const password = formData.get("password") as string;
|
||||||
|
|
||||||
|
const newConfig = {
|
||||||
|
serverUrl: serverUrl.trim(),
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/komga/config", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: newConfig.serverUrl,
|
||||||
|
username: newConfig.username,
|
||||||
|
password: newConfig.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.error || "Erreur lors de la sauvegarde de la configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
const komgaConfig = {
|
||||||
|
serverUrl: newConfig.serverUrl,
|
||||||
|
credentials: {
|
||||||
|
username: newConfig.username,
|
||||||
|
password: newConfig.password,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setConfig(newConfig);
|
||||||
|
|
||||||
|
// Émettre un événement pour notifier les autres composants
|
||||||
|
const configChangeEvent = new CustomEvent("komga-config-changed", { detail: komgaConfig });
|
||||||
|
window.dispatchEvent(configChangeEvent);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Configuration sauvegardée",
|
||||||
|
description: "La configuration a été sauvegardée avec succès",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la sauvegarde:", error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Erreur",
|
||||||
|
description:
|
||||||
|
error instanceof Error ? error.message : "Une erreur est survenue lors de la sauvegarde",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
setConfig((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTTLChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
setTTLConfig((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[name]: parseInt(value || "0", 10),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveTTL = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setSuccess(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/komga/ttl-config", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(ttlConfig),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.error || "Erreur lors de la sauvegarde de la configuration TTL");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Configuration TTL sauvegardée",
|
||||||
|
description: "La configuration des TTL a été sauvegardée avec succès",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la sauvegarde:", error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Erreur",
|
||||||
|
description:
|
||||||
|
error instanceof Error ? error.message : "Une erreur est survenue lors de la sauvegarde",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-3xl mx-auto py-8 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-3xl font-bold">Préférences</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages de succès/erreur */}
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-destructive/15 p-4">
|
||||||
|
<p className="text-sm text-destructive">{error.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<div className="rounded-md bg-green-500/15 p-4">
|
||||||
|
<p className="text-sm text-green-500">{success}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-6">
|
||||||
|
{/* Section Configuration Komga */}
|
||||||
|
<div className="rounded-lg border bg-card text-card-foreground shadow-sm">
|
||||||
|
<div className="p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||||
|
<Network className="h-5 w-5" />
|
||||||
|
Configuration Komga
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Configurez les informations de connexion à votre serveur Komga.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Formulaire de configuration */}
|
||||||
|
<form onSubmit={handleSave} className="space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="serverUrl" className="text-sm font-medium">
|
||||||
|
L'URL du serveur
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="serverUrl"
|
||||||
|
name="serverUrl"
|
||||||
|
required
|
||||||
|
value={config.serverUrl}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="username" className="text-sm font-medium">
|
||||||
|
L'adresse email de connexion
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
required
|
||||||
|
value={config.username}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="password" className="text-sm font-medium">
|
||||||
|
Mot de passe
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
value={config.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSaving}
|
||||||
|
className="flex-1 inline-flex items-center justify-center rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Sauvegarde...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Sauvegarder"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleTest}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex-1 inline-flex items-center justify-center rounded-md bg-secondary px-3 py-2 text-sm font-medium text-secondary-foreground ring-offset-background transition-colors hover:bg-secondary/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Test en cours...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Tester la connexion"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section Configuration du Cache */}
|
||||||
|
<div className="rounded-lg border bg-card text-card-foreground shadow-sm">
|
||||||
|
<div className="p-5 space-y-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||||
|
<Trash2 className="h-5 w-5" />
|
||||||
|
Configuration du Cache
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Gérez les paramètres de mise en cache des données.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Formulaire TTL */}
|
||||||
|
<form onSubmit={handleSaveTTL} className="space-y-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="defaultTTL" className="text-sm font-medium">
|
||||||
|
TTL par défaut (minutes)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="defaultTTL"
|
||||||
|
name="defaultTTL"
|
||||||
|
min="1"
|
||||||
|
value={ttlConfig.defaultTTL}
|
||||||
|
onChange={handleTTLChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="homeTTL" className="text-sm font-medium">
|
||||||
|
TTL page d'accueil
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="homeTTL"
|
||||||
|
name="homeTTL"
|
||||||
|
min="1"
|
||||||
|
value={ttlConfig.homeTTL}
|
||||||
|
onChange={handleTTLChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="librariesTTL" className="text-sm font-medium">
|
||||||
|
TTL bibliothèques
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="librariesTTL"
|
||||||
|
name="librariesTTL"
|
||||||
|
min="1"
|
||||||
|
value={ttlConfig.librariesTTL}
|
||||||
|
onChange={handleTTLChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="seriesTTL" className="text-sm font-medium">
|
||||||
|
TTL séries
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="seriesTTL"
|
||||||
|
name="seriesTTL"
|
||||||
|
min="1"
|
||||||
|
value={ttlConfig.seriesTTL}
|
||||||
|
onChange={handleTTLChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="booksTTL" className="text-sm font-medium">
|
||||||
|
TTL tomes
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="booksTTL"
|
||||||
|
name="booksTTL"
|
||||||
|
min="1"
|
||||||
|
value={ttlConfig.booksTTL}
|
||||||
|
onChange={handleTTLChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="imagesTTL" className="text-sm font-medium">
|
||||||
|
TTL images
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="imagesTTL"
|
||||||
|
name="imagesTTL"
|
||||||
|
min="1"
|
||||||
|
value={ttlConfig.imagesTTL}
|
||||||
|
onChange={handleTTLChange}
|
||||||
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex-1 inline-flex items-center justify-center rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Sauvegarder les TTL
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClearCache}
|
||||||
|
disabled={isCacheClearing}
|
||||||
|
className="flex-1 inline-flex items-center justify-center rounded-md bg-destructive px-3 py-2 text-sm font-medium text-destructive-foreground ring-offset-background transition-colors hover:bg-destructive/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isCacheClearing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Suppression...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Vider le cache"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/components/ui/tabs.tsx
Normal file
54
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
35
src/lib/models/config.model.ts
Normal file
35
src/lib/models/config.model.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
|
const configSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Middleware pour mettre à jour le champ updatedAt avant la sauvegarde
|
||||||
|
configSchema.pre("save", function (next) {
|
||||||
|
this.updatedAt = new Date();
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KomgaConfig =
|
||||||
|
mongoose.models.KomgaConfig || mongoose.model("KomgaConfig", configSchema);
|
||||||
23
src/lib/models/favorite.model.ts
Normal file
23
src/lib/models/favorite.model.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
|
const favoriteSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
seriesId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Index composé pour s'assurer qu'un utilisateur ne peut pas avoir deux fois le même favori
|
||||||
|
favoriteSchema.index({ userId: 1, seriesId: 1 }, { unique: true });
|
||||||
|
|
||||||
|
export const FavoriteModel = mongoose.models.Favorite || mongoose.model("Favorite", favoriteSchema);
|
||||||
46
src/lib/models/ttl-config.model.ts
Normal file
46
src/lib/models/ttl-config.model.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
|
const ttlConfigSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
userId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
},
|
||||||
|
defaultTTL: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
homeTTL: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
librariesTTL: {
|
||||||
|
type: Number,
|
||||||
|
default: 1440,
|
||||||
|
},
|
||||||
|
seriesTTL: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
booksTTL: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
imagesTTL: {
|
||||||
|
type: Number,
|
||||||
|
default: 1440,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Middleware pour mettre à jour le champ updatedAt avant la sauvegarde
|
||||||
|
ttlConfigSchema.pre("save", function (next) {
|
||||||
|
this.updatedAt = new Date();
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TTLConfig = mongoose.models.TTLConfig || mongoose.model("TTLConfig", ttlConfigSchema);
|
||||||
36
src/lib/models/user.model.ts
Normal file
36
src/lib/models/user.model.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
|
const userSchema = new mongoose.Schema(
|
||||||
|
{
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
lowercase: true,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
type: [String],
|
||||||
|
default: ["ROLE_USER"],
|
||||||
|
},
|
||||||
|
authenticated: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Middleware pour mettre à jour le champ updatedAt avant la sauvegarde
|
||||||
|
userSchema.pre("save", function (next) {
|
||||||
|
this.updatedAt = new Date();
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UserModel = mongoose.models.User || mongoose.model("User", userSchema);
|
||||||
51
src/lib/mongodb.ts
Normal file
51
src/lib/mongodb.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
|
const MONGODB_URI = process.env.MONGODB_URI;
|
||||||
|
|
||||||
|
if (!MONGODB_URI) {
|
||||||
|
throw new Error(
|
||||||
|
"Veuillez définir la variable d'environnement MONGODB_URI dans votre fichier .env"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MongooseCache {
|
||||||
|
conn: typeof mongoose | null;
|
||||||
|
promise: Promise<typeof mongoose> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var mongoose: MongooseCache | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cached: MongooseCache = global.mongoose || { conn: null, promise: null };
|
||||||
|
|
||||||
|
if (!global.mongoose) {
|
||||||
|
global.mongoose = { conn: null, promise: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectDB(): Promise<typeof mongoose> {
|
||||||
|
if (cached.conn) {
|
||||||
|
return cached.conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cached.promise) {
|
||||||
|
const opts = {
|
||||||
|
bufferCommands: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
cached.promise = mongoose.connect(MONGODB_URI!, opts).then((mongoose) => {
|
||||||
|
return mongoose;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cached.conn = await cached.promise;
|
||||||
|
} catch (e) {
|
||||||
|
cached.promise = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached.conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connectDB;
|
||||||
90
src/lib/services/auth-server.service.ts
Normal file
90
src/lib/services/auth-server.service.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { cookies } from "next/headers";
|
||||||
|
import connectDB from "@/lib/mongodb";
|
||||||
|
import { UserModel } from "@/lib/models/user.model";
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
roles: string[];
|
||||||
|
authenticated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthServerService {
|
||||||
|
static async createUser(email: string, password: string): Promise<UserData> {
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
// Check if user already exists
|
||||||
|
const existingUser = await UserModel.findOne({ email: email.toLowerCase() });
|
||||||
|
if (existingUser) {
|
||||||
|
throw new Error("EMAIL_EXISTS");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new user
|
||||||
|
const user = await UserModel.create({
|
||||||
|
email: email.toLowerCase(),
|
||||||
|
password,
|
||||||
|
roles: ["ROLE_USER"],
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userData: UserData = {
|
||||||
|
id: user._id.toString(),
|
||||||
|
email: user.email,
|
||||||
|
roles: user.roles,
|
||||||
|
authenticated: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return userData;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setUserCookie(userData: UserData): void {
|
||||||
|
// Encode user data in base64
|
||||||
|
const encodedUserData = Buffer.from(JSON.stringify(userData)).toString("base64");
|
||||||
|
|
||||||
|
// Set cookie with user data
|
||||||
|
cookies().set("stripUser", encodedUserData, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
sameSite: "lax",
|
||||||
|
path: "/",
|
||||||
|
maxAge: 24 * 60 * 60, // 24 hours by default for new users
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCurrentUser(): UserData | null {
|
||||||
|
const userCookie = cookies().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();
|
||||||
|
|
||||||
|
const user = await UserModel.findOne({ email: email.toLowerCase() });
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("INVALID_CREDENTIALS");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.password !== password) {
|
||||||
|
throw new Error("INVALID_CREDENTIALS");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData: UserData = {
|
||||||
|
id: user._id.toString(),
|
||||||
|
email: user.email,
|
||||||
|
roles: user.roles,
|
||||||
|
authenticated: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return userData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { AuthError } from "@/types/auth";
|
import { AuthError } from "@/types/auth";
|
||||||
import { storageService } from "./storage.service";
|
|
||||||
|
|
||||||
interface AuthUser {
|
interface AuthUser {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -7,19 +8,6 @@ interface AuthUser {
|
|||||||
roles: string[];
|
roles: string[];
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utilisateur de développement
|
|
||||||
const DEV_USER = {
|
|
||||||
email: "demo@stripstream.local",
|
|
||||||
password: "demo123",
|
|
||||||
userData: {
|
|
||||||
id: "1",
|
|
||||||
email: "demo@stripstream.local",
|
|
||||||
roles: ["ROLE_USER"],
|
|
||||||
authenticated: true,
|
|
||||||
} as AuthUser,
|
|
||||||
};
|
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
private static instance: AuthService;
|
private static instance: AuthService;
|
||||||
|
|
||||||
@@ -36,38 +24,65 @@ class AuthService {
|
|||||||
* Authentifie un utilisateur
|
* Authentifie un utilisateur
|
||||||
*/
|
*/
|
||||||
async login(email: string, password: string, remember: boolean = false): Promise<void> {
|
async login(email: string, password: string, remember: boolean = false): Promise<void> {
|
||||||
// En développement, on vérifie juste l'utilisateur de démo
|
try {
|
||||||
if (email === DEV_USER.email && password === DEV_USER.password) {
|
const response = await fetch("/api/auth/login", {
|
||||||
storageService.setUserData(DEV_USER.userData, remember);
|
method: "POST",
|
||||||
return;
|
headers: {
|
||||||
}
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email, password, remember }),
|
||||||
|
});
|
||||||
|
|
||||||
throw {
|
if (!response.ok) {
|
||||||
code: "INVALID_CREDENTIALS",
|
const data = await response.json();
|
||||||
message: "Email ou mot de passe incorrect",
|
throw data.error;
|
||||||
} as AuthError;
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as AuthError).code) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw {
|
||||||
|
code: "SERVER_ERROR",
|
||||||
|
message: "Une erreur est survenue lors de la connexion",
|
||||||
|
} as AuthError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 AuthError).code) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw {
|
||||||
|
code: "SERVER_ERROR",
|
||||||
|
message: "Une erreur est survenue lors de l'inscription",
|
||||||
|
} as AuthError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Déconnecte l'utilisateur
|
* Déconnecte l'utilisateur
|
||||||
*/
|
*/
|
||||||
logout(): void {
|
async logout(): Promise<void> {
|
||||||
storageService.clear();
|
await fetch("/api/auth/logout", {
|
||||||
}
|
method: "POST",
|
||||||
|
});
|
||||||
/**
|
|
||||||
* Vérifie si l'utilisateur est connecté
|
|
||||||
*/
|
|
||||||
isAuthenticated(): boolean {
|
|
||||||
const user = storageService.getUserData<AuthUser>();
|
|
||||||
return !!user?.authenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère l'utilisateur connecté
|
|
||||||
*/
|
|
||||||
getCurrentUser(): AuthUser | null {
|
|
||||||
return storageService.getUserData<AuthUser>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { AuthConfig } from "@/types/auth";
|
import { AuthConfig } from "@/types/auth";
|
||||||
import { serverCacheService } from "./server-cache.service";
|
import { serverCacheService } from "./server-cache.service";
|
||||||
import { komgaConfigService } from "./komga-config.service";
|
import { ConfigDBService } from "./config-db.service";
|
||||||
|
|
||||||
// Types de cache disponibles
|
// Types de cache disponibles
|
||||||
export type CacheType = "DEFAULT" | "HOME" | "LIBRARIES" | "SERIES" | "BOOKS" | "IMAGES";
|
export type CacheType = "DEFAULT" | "HOME" | "LIBRARIES" | "SERIES" | "BOOKS" | "IMAGES";
|
||||||
|
|
||||||
export abstract class BaseApiService {
|
export abstract class BaseApiService {
|
||||||
protected static async getKomgaConfig(): Promise<AuthConfig> {
|
protected static async getKomgaConfig(): Promise<AuthConfig> {
|
||||||
const cookiesStore = cookies();
|
try {
|
||||||
return komgaConfigService.validateAndGetConfig(cookiesStore);
|
const config = await ConfigDBService.getConfig();
|
||||||
|
return {
|
||||||
|
serverUrl: config.url,
|
||||||
|
credentials: {
|
||||||
|
username: config.username,
|
||||||
|
password: config.password,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération de la configuration:", error);
|
||||||
|
throw new Error("Configuration Komga non trouvée");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static getAuthHeaders(config: AuthConfig): Headers {
|
protected static getAuthHeaders(config: AuthConfig): Headers {
|
||||||
|
|||||||
116
src/lib/services/config-db.service.ts
Normal file
116
src/lib/services/config-db.service.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { cookies } from "next/headers";
|
||||||
|
import connectDB from "@/lib/mongodb";
|
||||||
|
import { KomgaConfig } from "@/lib/models/config.model";
|
||||||
|
import { TTLConfig } from "@/lib/models/ttl-config.model";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface KomgaConfigData {
|
||||||
|
url: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TTLConfigData {
|
||||||
|
defaultTTL: number;
|
||||||
|
homeTTL: number;
|
||||||
|
librariesTTL: number;
|
||||||
|
seriesTTL: number;
|
||||||
|
booksTTL: number;
|
||||||
|
imagesTTL: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConfigDBService {
|
||||||
|
private static async getCurrentUser(): Promise<User> {
|
||||||
|
const userCookie = cookies().get("stripUser");
|
||||||
|
|
||||||
|
if (!userCookie) {
|
||||||
|
throw new Error("Utilisateur non authentifié");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(atob(userCookie.value));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération de l'utilisateur depuis le cookie:", error);
|
||||||
|
throw new Error("Utilisateur non authentifié");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveConfig(data: KomgaConfigData) {
|
||||||
|
const user = await this.getCurrentUser();
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
const config = await KomgaConfig.findOneAndUpdate(
|
||||||
|
{ userId: user.id },
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
url: data.url,
|
||||||
|
username: data.username,
|
||||||
|
password: data.password,
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getConfig() {
|
||||||
|
const user = await this.getCurrentUser();
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
const config = await KomgaConfig.findOne({ userId: user.id });
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("Configuration non trouvée");
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async saveTTLConfig(data: TTLConfigData) {
|
||||||
|
const user = await this.getCurrentUser();
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
const config = await TTLConfig.findOneAndUpdate(
|
||||||
|
{ userId: user.id },
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getTTLConfig() {
|
||||||
|
const user = await this.getCurrentUser();
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
const config = await TTLConfig.findOne({ userId: user.id });
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
// Retourner la configuration par défaut si aucune configuration n'existe
|
||||||
|
return {
|
||||||
|
defaultTTL: 5,
|
||||||
|
homeTTL: 5,
|
||||||
|
librariesTTL: 1440,
|
||||||
|
seriesTTL: 5,
|
||||||
|
booksTTL: 5,
|
||||||
|
imagesTTL: 1440,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultTTL: config.defaultTTL,
|
||||||
|
homeTTL: config.homeTTL,
|
||||||
|
librariesTTL: config.librariesTTL,
|
||||||
|
seriesTTL: config.seriesTTL,
|
||||||
|
booksTTL: config.booksTTL,
|
||||||
|
imagesTTL: config.imagesTTL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,111 @@
|
|||||||
import { storageService } from "./storage.service";
|
import { cookies } from "next/headers";
|
||||||
|
import connectDB from "@/lib/mongodb";
|
||||||
|
import { FavoriteModel } from "@/lib/models/favorite.model";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class FavoriteService {
|
export class FavoriteService {
|
||||||
private static readonly FAVORITES_CHANGE_EVENT = "favoritesChanged";
|
private static readonly FAVORITES_CHANGE_EVENT = "favoritesChanged";
|
||||||
|
|
||||||
private static dispatchFavoritesChanged() {
|
private static dispatchFavoritesChanged() {
|
||||||
// Dispatch l'événement pour notifier les changements
|
// Dispatch l'événement pour notifier les changements
|
||||||
window.dispatchEvent(new Event(FavoriteService.FAVORITES_CHANGE_EVENT));
|
if (typeof window !== "undefined") {
|
||||||
|
window.dispatchEvent(new Event(FavoriteService.FAVORITES_CHANGE_EVENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getCurrentUser(): Promise<User> {
|
||||||
|
const userCookie = cookies().get("stripUser");
|
||||||
|
|
||||||
|
if (!userCookie) {
|
||||||
|
throw new Error("Utilisateur non authentifié");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(atob(userCookie.value));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération de l'utilisateur depuis le cookie:", error);
|
||||||
|
throw new Error("Utilisateur non authentifié");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vérifie si une série est dans les favoris
|
* Vérifie si une série est dans les favoris
|
||||||
*/
|
*/
|
||||||
static isFavorite(seriesId: string): boolean {
|
static async isFavorite(seriesId: string): Promise<boolean> {
|
||||||
return storageService.isFavorite(seriesId);
|
try {
|
||||||
|
const user = await this.getCurrentUser();
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
const favorite = await FavoriteModel.findOne({
|
||||||
|
userId: user.id,
|
||||||
|
seriesId: seriesId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!favorite;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la vérification du favori:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ajoute une série aux favoris
|
* Ajoute une série aux favoris
|
||||||
*/
|
*/
|
||||||
static addToFavorites(seriesId: string): void {
|
static async addToFavorites(seriesId: string): Promise<void> {
|
||||||
storageService.addFavorite(seriesId);
|
try {
|
||||||
this.dispatchFavoritesChanged();
|
const user = await this.getCurrentUser();
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
await FavoriteModel.findOneAndUpdate(
|
||||||
|
{ userId: user.id, seriesId },
|
||||||
|
{ userId: user.id, seriesId },
|
||||||
|
{ upsert: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.dispatchFavoritesChanged();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de l'ajout aux favoris:", error);
|
||||||
|
throw new Error("Erreur lors de l'ajout aux favoris");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retire une série des favoris
|
* Retire une série des favoris
|
||||||
*/
|
*/
|
||||||
static removeFromFavorites(seriesId: string): void {
|
static async removeFromFavorites(seriesId: string): Promise<void> {
|
||||||
storageService.removeFavorite(seriesId);
|
try {
|
||||||
this.dispatchFavoritesChanged();
|
const user = await this.getCurrentUser();
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
await FavoriteModel.findOneAndDelete({
|
||||||
|
userId: user.id,
|
||||||
|
seriesId,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dispatchFavoritesChanged();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la suppression des favoris:", error);
|
||||||
|
throw new Error("Erreur lors de la suppression des favoris");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère tous les IDs des séries favorites
|
* Récupère tous les IDs des séries favorites
|
||||||
*/
|
*/
|
||||||
static getAllFavoriteIds(): string[] {
|
static async getAllFavoriteIds(): Promise<string[]> {
|
||||||
return storageService.getFavorites();
|
try {
|
||||||
|
const user = await this.getCurrentUser();
|
||||||
|
await connectDB();
|
||||||
|
|
||||||
|
const favorites = await FavoriteModel.find({ userId: user.id });
|
||||||
|
return favorites.map((favorite) => favorite.seriesId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la récupération des favoris:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { AuthConfig } from "@/types/auth";
|
|
||||||
import { storageService } from "./storage.service";
|
|
||||||
import { STORAGE_KEYS } from "@/lib/constants";
|
|
||||||
|
|
||||||
const { CREDENTIALS } = STORAGE_KEYS;
|
|
||||||
|
|
||||||
class KomgaConfigService {
|
|
||||||
private static instance: KomgaConfigService;
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
public static getInstance(): KomgaConfigService {
|
|
||||||
if (!KomgaConfigService.instance) {
|
|
||||||
KomgaConfigService.instance = new KomgaConfigService();
|
|
||||||
}
|
|
||||||
return KomgaConfigService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère la configuration Komga (fonctionne côté client et serveur)
|
|
||||||
*/
|
|
||||||
getConfig(serverCookies?: any): AuthConfig | null {
|
|
||||||
// Côté serveur
|
|
||||||
if (typeof window === "undefined" && serverCookies) {
|
|
||||||
try {
|
|
||||||
const configCookie = serverCookies.get(CREDENTIALS)?.value;
|
|
||||||
if (!configCookie) return null;
|
|
||||||
return JSON.parse(atob(configCookie));
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"KomgaConfigService - Erreur lors de la récupération de la config côté serveur:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Côté client
|
|
||||||
return storageService.getCredentials();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Définit la configuration Komga (côté client uniquement)
|
|
||||||
*/
|
|
||||||
setConfig(config: AuthConfig, remember: boolean = false): void {
|
|
||||||
if (typeof window === "undefined") {
|
|
||||||
console.warn("KomgaConfigService - setConfig ne peut être utilisé que côté client");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const storage = remember ? localStorage : sessionStorage;
|
|
||||||
const encoded = btoa(JSON.stringify(config));
|
|
||||||
|
|
||||||
// Stocker dans le storage
|
|
||||||
storage.setItem(CREDENTIALS, encoded);
|
|
||||||
|
|
||||||
// Définir le cookie
|
|
||||||
const cookieValue = `${CREDENTIALS}=${encoded}; path=/; samesite=strict`;
|
|
||||||
const maxAge = remember ? `; max-age=${30 * 24 * 60 * 60}` : "";
|
|
||||||
document.cookie = cookieValue + maxAge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie si la configuration est valide
|
|
||||||
*/
|
|
||||||
isConfigValid(config: AuthConfig | null): boolean {
|
|
||||||
if (!config) return false;
|
|
||||||
return !!(config.serverUrl && config.credentials?.username && config.credentials?.password);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Efface la configuration
|
|
||||||
*/
|
|
||||||
clearConfig(): void {
|
|
||||||
if (typeof window === "undefined") return;
|
|
||||||
|
|
||||||
localStorage.removeItem(CREDENTIALS);
|
|
||||||
sessionStorage.removeItem(CREDENTIALS);
|
|
||||||
document.cookie = `${CREDENTIALS}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère l'URL du serveur à partir de la configuration
|
|
||||||
*/
|
|
||||||
getServerUrl(serverCookies?: any): string | null {
|
|
||||||
const config = this.getConfig(serverCookies);
|
|
||||||
return config?.serverUrl || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les credentials à partir de la configuration
|
|
||||||
*/
|
|
||||||
getCredentials(serverCookies?: any): { username: string; password: string } | null {
|
|
||||||
const config = this.getConfig(serverCookies);
|
|
||||||
return config?.credentials || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construit une URL complète pour l'API Komga
|
|
||||||
*/
|
|
||||||
buildApiUrl(path: string, serverCookies?: any): string {
|
|
||||||
const serverUrl = this.getServerUrl(serverCookies);
|
|
||||||
if (!serverUrl) throw new Error("URL du serveur non disponible");
|
|
||||||
return `${serverUrl}/api/v1/${path}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Génère les en-têtes d'authentification pour les requêtes
|
|
||||||
*/
|
|
||||||
getAuthHeaders(serverCookies?: any): Headers {
|
|
||||||
const credentials = this.getCredentials(serverCookies);
|
|
||||||
if (!credentials) throw new Error("Credentials non disponibles");
|
|
||||||
|
|
||||||
const auth = Buffer.from(`${credentials.username}:${credentials.password}`).toString("base64");
|
|
||||||
const headers = new Headers();
|
|
||||||
headers.set("Authorization", `Basic ${auth}`);
|
|
||||||
headers.set("Accept", "application/json");
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vérifie et récupère la configuration complète, lance une erreur si invalide
|
|
||||||
*/
|
|
||||||
validateAndGetConfig(serverCookies?: any): AuthConfig {
|
|
||||||
const config = this.getConfig(serverCookies);
|
|
||||||
if (!this.isConfigValid(config)) {
|
|
||||||
throw new Error("Configuration Komga manquante ou invalide");
|
|
||||||
}
|
|
||||||
return config as AuthConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const komgaConfigService = KomgaConfigService.getInstance();
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import { AuthConfig } from "@/types/auth";
|
|
||||||
import { STORAGE_KEYS } from "@/lib/constants";
|
|
||||||
|
|
||||||
const {
|
|
||||||
CREDENTIALS: KOMGACREDENTIALS_KEY,
|
|
||||||
USER: USER_KEY,
|
|
||||||
TTL_CONFIG: TTL_CONFIG_KEY,
|
|
||||||
FAVORITES: FAVORITES_KEY,
|
|
||||||
} = STORAGE_KEYS;
|
|
||||||
|
|
||||||
interface TTLConfig {
|
|
||||||
defaultTTL: number;
|
|
||||||
homeTTL: number;
|
|
||||||
librariesTTL: number;
|
|
||||||
seriesTTL: number;
|
|
||||||
booksTTL: number;
|
|
||||||
imagesTTL: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class StorageService {
|
|
||||||
private static instance: StorageService;
|
|
||||||
|
|
||||||
private constructor() {}
|
|
||||||
|
|
||||||
public static getInstance(): StorageService {
|
|
||||||
if (!StorageService.instance) {
|
|
||||||
StorageService.instance = new StorageService();
|
|
||||||
}
|
|
||||||
return StorageService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stocke les credentials de manière sécurisée
|
|
||||||
*/
|
|
||||||
setKomgaConfig(config: AuthConfig, remember: boolean = false): void {
|
|
||||||
const storage = remember ? localStorage : sessionStorage;
|
|
||||||
|
|
||||||
// Encodage basique des credentials en base64
|
|
||||||
const encoded = btoa(JSON.stringify(config));
|
|
||||||
console.log("StorageService - Stockage des credentials:", {
|
|
||||||
storage: remember ? "localStorage" : "sessionStorage",
|
|
||||||
config: {
|
|
||||||
serverUrl: config.serverUrl,
|
|
||||||
hasCredentials: !!config.credentials,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
storage.setItem(KOMGACREDENTIALS_KEY, encoded);
|
|
||||||
|
|
||||||
// Définir aussi un cookie pour le middleware
|
|
||||||
const cookieValue = `${KOMGACREDENTIALS_KEY}=${encoded}; path=/; samesite=strict`;
|
|
||||||
const maxAge = remember ? `; max-age=${30 * 24 * 60 * 60}` : "";
|
|
||||||
document.cookie = cookieValue + maxAge;
|
|
||||||
|
|
||||||
console.log("StorageService - Cookie défini:", cookieValue + maxAge);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les credentials stockés
|
|
||||||
*/
|
|
||||||
getCredentials(): AuthConfig | null {
|
|
||||||
if (typeof window === "undefined") return null;
|
|
||||||
|
|
||||||
const storage =
|
|
||||||
localStorage.getItem(KOMGACREDENTIALS_KEY) || sessionStorage.getItem(KOMGACREDENTIALS_KEY);
|
|
||||||
console.log("StorageService - Lecture des credentials:", {
|
|
||||||
fromLocalStorage: !!localStorage.getItem(KOMGACREDENTIALS_KEY),
|
|
||||||
fromSessionStorage: !!sessionStorage.getItem(KOMGACREDENTIALS_KEY),
|
|
||||||
value: storage,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!storage) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(atob(storage));
|
|
||||||
console.log("StorageService - Credentials décodés:", {
|
|
||||||
serverUrl: config.serverUrl,
|
|
||||||
hasCredentials: !!config.credentials,
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("StorageService - Erreur de décodage des credentials:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stocke les données utilisateur
|
|
||||||
*/
|
|
||||||
setUserData<T>(data: T, remember: boolean = false): void {
|
|
||||||
const storage = remember ? localStorage : sessionStorage;
|
|
||||||
const encoded = btoa(JSON.stringify(data));
|
|
||||||
storage.setItem(USER_KEY, encoded);
|
|
||||||
|
|
||||||
// Définir aussi un cookie pour le middleware
|
|
||||||
document.cookie = `${USER_KEY}=${encoded}; path=/; samesite=strict; ${
|
|
||||||
remember ? `max-age=${30 * 24 * 60 * 60}` : ""
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère les données utilisateur
|
|
||||||
*/
|
|
||||||
getUserData<T>(): T | null {
|
|
||||||
if (typeof window === "undefined") return null;
|
|
||||||
|
|
||||||
const storage = localStorage.getItem(USER_KEY) || sessionStorage.getItem(USER_KEY);
|
|
||||||
if (!storage) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(atob(storage));
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stocke la configuration des TTL
|
|
||||||
*/
|
|
||||||
setTTLConfig(config: TTLConfig): void {
|
|
||||||
localStorage.setItem(TTL_CONFIG_KEY, JSON.stringify(config));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Récupère la configuration des TTL
|
|
||||||
*/
|
|
||||||
getTTLConfig(): TTLConfig | null {
|
|
||||||
const stored = localStorage.getItem(TTL_CONFIG_KEY);
|
|
||||||
if (!stored) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(stored);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Efface toutes les données stockées
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
localStorage.removeItem(KOMGACREDENTIALS_KEY);
|
|
||||||
localStorage.removeItem(USER_KEY);
|
|
||||||
sessionStorage.removeItem(KOMGACREDENTIALS_KEY);
|
|
||||||
sessionStorage.removeItem(USER_KEY);
|
|
||||||
document.cookie = `${KOMGACREDENTIALS_KEY}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
||||||
document.cookie = `${USER_KEY}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getUser() {
|
|
||||||
try {
|
|
||||||
const userStr = localStorage.getItem(USER_KEY);
|
|
||||||
if (!userStr) return null;
|
|
||||||
return JSON.parse(atob(userStr));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la récupération de l'utilisateur:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clearAll() {
|
|
||||||
localStorage.removeItem(USER_KEY);
|
|
||||||
localStorage.removeItem(KOMGACREDENTIALS_KEY);
|
|
||||||
localStorage.removeItem(TTL_CONFIG_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeys() {
|
|
||||||
return {
|
|
||||||
credentials: KOMGACREDENTIALS_KEY,
|
|
||||||
user: USER_KEY,
|
|
||||||
ttlConfig: TTL_CONFIG_KEY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getFavorites(): string[] {
|
|
||||||
try {
|
|
||||||
const favorites = localStorage.getItem(FAVORITES_KEY);
|
|
||||||
return favorites ? JSON.parse(favorites) : [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la récupération des favoris:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addFavorite(seriesId: string): void {
|
|
||||||
try {
|
|
||||||
const favorites = this.getFavorites();
|
|
||||||
if (!favorites.includes(seriesId)) {
|
|
||||||
favorites.push(seriesId);
|
|
||||||
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de l'ajout aux favoris:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFavorite(seriesId: string): void {
|
|
||||||
try {
|
|
||||||
const favorites = this.getFavorites();
|
|
||||||
const index = favorites.indexOf(seriesId);
|
|
||||||
if (index > -1) {
|
|
||||||
favorites.splice(index, 1);
|
|
||||||
localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la suppression des favoris:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isFavorite(seriesId: string): boolean {
|
|
||||||
try {
|
|
||||||
const favorites = this.getFavorites();
|
|
||||||
return favorites.includes(seriesId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erreur lors de la vérification des favoris:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const storageService = StorageService.getInstance();
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { komgaConfigService } from "@/lib/services/komga-config.service";
|
|
||||||
|
|
||||||
// Routes qui ne nécessitent pas d'authentification
|
// Routes qui ne nécessitent pas d'authentification
|
||||||
const publicRoutes = ["/login", "/register", "/images"];
|
const publicRoutes = ["/login", "/register", "/images"];
|
||||||
@@ -15,29 +14,18 @@ export function middleware(request: NextRequest) {
|
|||||||
if (
|
if (
|
||||||
publicRoutes.includes(pathname) ||
|
publicRoutes.includes(pathname) ||
|
||||||
publicApiRoutes.includes(pathname) ||
|
publicApiRoutes.includes(pathname) ||
|
||||||
pathname.startsWith("/images/")
|
pathname.startsWith("/images/") ||
|
||||||
|
pathname.startsWith("/_next/")
|
||||||
) {
|
) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si c'est une route d'API
|
// Pour toutes les routes protégées, vérifier la présence de l'utilisateur
|
||||||
if (pathname.startsWith("/api/")) {
|
|
||||||
// Vérifier la configuration Komga
|
|
||||||
const config = komgaConfigService.getConfig(request.cookies);
|
|
||||||
|
|
||||||
if (!komgaConfigService.isConfigValid(config)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Configuration Komga manquante ou invalide" },
|
|
||||||
{ status: 401 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pour les routes protégées, vérifier la présence de l'utilisateur
|
|
||||||
const user = request.cookies.get("stripUser");
|
const user = request.cookies.get("stripUser");
|
||||||
if (!user) {
|
if (!user || !user.value) {
|
||||||
|
if (pathname.startsWith("/api/")) {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
const loginUrl = new URL("/login", request.url);
|
const loginUrl = new URL("/login", request.url);
|
||||||
loginUrl.searchParams.set("from", pathname);
|
loginUrl.searchParams.set("from", pathname);
|
||||||
return NextResponse.redirect(loginUrl);
|
return NextResponse.redirect(loginUrl);
|
||||||
@@ -45,10 +33,14 @@ export function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const userData = JSON.parse(atob(user.value));
|
const userData = JSON.parse(atob(user.value));
|
||||||
if (!userData.authenticated) {
|
if (!userData || !userData.authenticated || !userData.id || !userData.email) {
|
||||||
throw new Error("User not authenticated");
|
throw new Error("Invalid user data");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Erreur de validation du cookie:", error);
|
||||||
|
if (pathname.startsWith("/api/")) {
|
||||||
|
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||||
|
}
|
||||||
const loginUrl = new URL("/login", request.url);
|
const loginUrl = new URL("/login", request.url);
|
||||||
loginUrl.searchParams.set("from", pathname);
|
loginUrl.searchParams.set("from", pathname);
|
||||||
return NextResponse.redirect(loginUrl);
|
return NextResponse.redirect(loginUrl);
|
||||||
@@ -66,7 +58,7 @@ export const config = {
|
|||||||
* 2. /_next/* (Next.js internals)
|
* 2. /_next/* (Next.js internals)
|
||||||
* 3. /fonts/* (inside public directory)
|
* 3. /fonts/* (inside public directory)
|
||||||
* 4. /images/* (inside public directory)
|
* 4. /images/* (inside public directory)
|
||||||
* 5. /favicon.ico, /sitemap.xml (public files)
|
* 5. /favicon.ico, sitemap.xml (public files)
|
||||||
*/
|
*/
|
||||||
"/((?!api/auth|_next/static|_next/image|fonts|images|favicon.ico|sitemap.xml).*)",
|
"/((?!api/auth|_next/static|_next/image|fonts|images|favicon.ico|sitemap.xml).*)",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,11 +19,4 @@ export interface AuthError {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthErrorCode =
|
export type AuthErrorCode = "INVALID_CREDENTIALS" | "SERVER_ERROR" | "EMAIL_EXISTS";
|
||||||
| "INVALID_CREDENTIALS"
|
|
||||||
| "INVALID_SERVER_URL"
|
|
||||||
| "SERVER_UNREACHABLE"
|
|
||||||
| "NETWORK_ERROR"
|
|
||||||
| "UNKNOWN_ERROR"
|
|
||||||
| "CACHE_CLEAR_ERROR"
|
|
||||||
| "TEST_CONNECTION_ERROR";
|
|
||||||
|
|||||||
Reference in New Issue
Block a user