Merge pull request #2 from julienfroidefond/refacto/confOnMongo
Refacto/conf on mongo
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
.gitignore
|
||||
node_modules
|
||||
.next
|
||||
.env
|
||||
.env.local
|
||||
.env.development.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
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com
|
||||
- NEXT_PUBLIC_API_URL= ${NEXT_PUBLIC_API_URL}
|
||||
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-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@types/mongoose": "^5.11.97",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.323.0",
|
||||
"mongoose": "^8.10.0",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sharp": "^0.33.2",
|
||||
"sonner": "^1.7.4",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.22.4"
|
||||
@@ -52,6 +55,17 @@
|
||||
"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": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
|
||||
@@ -654,6 +668,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": {
|
||||
"version": "14.2.23",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz",
|
||||
@@ -859,6 +881,14 @@
|
||||
"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": {
|
||||
"version": "0.11.0",
|
||||
"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": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz",
|
||||
@@ -1516,6 +1575,15 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "20.17.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz",
|
||||
@@ -1560,6 +1628,19 @@
|
||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
||||
"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": {
|
||||
"version": "6.21.0",
|
||||
"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_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": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -2472,6 +2561,14 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2570,7 +2667,6 @@
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -4471,6 +4567,14 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -4540,6 +4644,14 @@
|
||||
"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": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -4662,6 +4774,11 @@
|
||||
"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": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -4716,11 +4833,104 @@
|
||||
"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": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"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": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
|
||||
@@ -4872,6 +5113,11 @@
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -5002,6 +5248,14 @@
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
@@ -5012,6 +5266,39 @@
|
||||
"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": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
@@ -5343,6 +5630,26 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -5353,6 +5660,11 @@
|
||||
"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": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -5369,7 +5681,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -5540,6 +5851,11 @@
|
||||
"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": {
|
||||
"version": "1.5.4",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
@@ -5965,15 +6286,6 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -5983,6 +6295,14 @@
|
||||
"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": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz",
|
||||
@@ -6399,6 +6719,17 @@
|
||||
"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": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
|
||||
@@ -6671,6 +7002,34 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -6885,6 +7244,11 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.7.0",
|
||||
"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-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@types/mongoose": "^5.11.97",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.323.0",
|
||||
"mongoose": "^8.10.0",
|
||||
"next": "^14.1.0",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^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 { ConfigDBService } from "@/lib/services/config-db.service";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const data = await request.json();
|
||||
console.log("Configuration Komga reçue:", data);
|
||||
|
||||
return NextResponse.json({ message: "Configuration reçue avec succès" }, { status: 200 });
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la réception de la configuration:", error);
|
||||
const mongoConfig = await ConfigDBService.saveConfig(data);
|
||||
// Convertir le document Mongoose en objet simple
|
||||
const config = {
|
||||
url: mongoConfig.url,
|
||||
username: mongoConfig.username,
|
||||
password: mongoConfig.password,
|
||||
userId: mongoConfig.userId,
|
||||
};
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la réception de la configuration" },
|
||||
{ status: 400 }
|
||||
{ message: "Configuration sauvegardée avec succès", config },
|
||||
{ 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 { komgaConfigService } from "@/lib/services/komga-config.service";
|
||||
import { LibraryService } from "@/lib/services/library.service";
|
||||
|
||||
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";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { authService } from "@/lib/services/auth.service";
|
||||
import { AuthError } from "@/types/auth";
|
||||
export const metadata: Metadata = {
|
||||
title: "Connexion",
|
||||
description: "Connectez-vous à votre compte StripStream",
|
||||
};
|
||||
|
||||
function LoginForm() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
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);
|
||||
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>
|
||||
);
|
||||
export default function LoginPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { from?: string; tab?: string };
|
||||
}) {
|
||||
return <LoginContent searchParams={searchParams} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { HomeContent } from "@/components/home/HomeContent";
|
||||
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";
|
||||
|
||||
export default async function HomePage() {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { PaginatedBookGrid } from "@/components/series/PaginatedBookGrid";
|
||||
import { SeriesHeader } from "@/components/series/SeriesHeader";
|
||||
import { komgaConfigService } from "@/lib/services/komga-config.service";
|
||||
import { SeriesService } from "@/lib/services/series.service";
|
||||
import { KomgaSeries } from "@/types/komga";
|
||||
|
||||
interface PageProps {
|
||||
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";
|
||||
import { Loader2, Network, Trash2 } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
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";
|
||||
export default async function SettingsPage() {
|
||||
let config = null;
|
||||
let ttlConfig = null;
|
||||
|
||||
interface ErrorMessage {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [isLoading, setIsLoading] = 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: "",
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const [ttlConfig, setTTLConfig] = useState({
|
||||
defaultTTL: 5,
|
||||
homeTTL: 5,
|
||||
librariesTTL: 1440,
|
||||
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>
|
||||
);
|
||||
try {
|
||||
// Récupérer la configuration Komga
|
||||
const mongoConfig = await ConfigDBService.getConfig();
|
||||
if (mongoConfig) {
|
||||
config = {
|
||||
url: mongoConfig.url,
|
||||
username: mongoConfig.username,
|
||||
password: mongoConfig.password,
|
||||
userId: mongoConfig.userId,
|
||||
};
|
||||
}
|
||||
|
||||
// Récupérer la configuration TTL
|
||||
ttlConfig = await ConfigDBService.getTTLConfig();
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération de la configuration:", error);
|
||||
// On ne fait rien si la config n'existe pas, on laissera le composant client gérer l'état initial
|
||||
}
|
||||
|
||||
return <ClientSettings initialConfig={config} initialTTLConfig={ttlConfig} />;
|
||||
}
|
||||
|
||||
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) {
|
||||
console.log("HeroSection - Séries reçues:", {
|
||||
count: series?.length || 0,
|
||||
firstSeries: series?.[0],
|
||||
});
|
||||
// console.log("HeroSection - Séries reçues:", {
|
||||
// count: series?.length || 0,
|
||||
// firstSeries: series?.[0],
|
||||
// });
|
||||
|
||||
return (
|
||||
<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
|
||||
console.log("HomeContent - Données reçues:", {
|
||||
ongoingCount: data.ongoing?.length || 0,
|
||||
recentlyReadCount: data.recentlyRead?.length || 0,
|
||||
onDeckCount: data.onDeck?.length || 0,
|
||||
});
|
||||
// console.log("HomeContent - Données reçues:", {
|
||||
// ongoingCount: data.ongoing?.length || 0,
|
||||
// recentlyReadCount: data.recentlyRead?.length || 0,
|
||||
// onDeckCount: data.onDeck?.length || 0,
|
||||
// });
|
||||
|
||||
return (
|
||||
<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 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 = () => {
|
||||
setIsSidebarOpen(false);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { BookOpen, Home, Library, Settings, LogOut, RefreshCw, Star } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
@@ -5,8 +7,6 @@ import { cn } from "@/lib/utils";
|
||||
import { authService } from "@/lib/services/auth.service";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||
import { storageService } from "@/lib/services/storage.service";
|
||||
import { FavoriteService } from "@/lib/services/favorite.service";
|
||||
|
||||
interface SidebarProps {
|
||||
isOpen: boolean;
|
||||
@@ -43,13 +43,20 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||
const fetchFavorites = useCallback(async () => {
|
||||
setIsLoadingFavorites(true);
|
||||
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) {
|
||||
setFavorites([]);
|
||||
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}`);
|
||||
if (!response.ok) return null;
|
||||
return response.json();
|
||||
@@ -69,7 +76,7 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||
useEffect(() => {
|
||||
fetchLibraries();
|
||||
fetchFavorites();
|
||||
}, []); // Suppression de la dépendance pathname
|
||||
}, [fetchLibraries, fetchFavorites]);
|
||||
|
||||
// Mettre à jour les favoris quand ils changent
|
||||
useEffect(() => {
|
||||
@@ -77,18 +84,10 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||
fetchFavorites();
|
||||
};
|
||||
|
||||
// Écouter les changements de favoris dans la même fenêtre
|
||||
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 () => {
|
||||
window.removeEventListener("favoritesChanged", handleFavoritesChange);
|
||||
window.removeEventListener("storage", handleFavoritesChange);
|
||||
};
|
||||
}, [fetchFavorites]);
|
||||
|
||||
@@ -99,7 +98,6 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
|
||||
|
||||
const handleLogout = () => {
|
||||
authService.logout();
|
||||
storageService.clearAll();
|
||||
setLibraries([]);
|
||||
setFavorites([]);
|
||||
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";
|
||||
|
||||
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 { useState, useEffect } from "react";
|
||||
import { FavoriteService } from "@/lib/services/favorite.service";
|
||||
import { Star, StarOff } from "lucide-react";
|
||||
import { Button } from "../ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -56,13 +54,27 @@ export const SeriesHeader = ({ series, onSeriesUpdate }: SeriesHeaderProps) => {
|
||||
const [readingStatus, setReadingStatus] = useState<ReadingStatusInfo>(
|
||||
getReadingStatusInfo(series)
|
||||
);
|
||||
const [isFavorite, setIsFavorite] = useState(FavoriteService.isFavorite(series.id));
|
||||
const [isFavorite, setIsFavorite] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Vérifier si la série est dans les favoris au chargement
|
||||
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);
|
||||
}, []);
|
||||
}, [series.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setReadingStatus(getReadingStatusInfo(series));
|
||||
@@ -82,18 +94,29 @@ export const SeriesHeader = ({ series, onSeriesUpdate }: SeriesHeaderProps) => {
|
||||
}
|
||||
}, [series.metadata.language]);
|
||||
|
||||
const handleToggleFavorite = () => {
|
||||
const handleToggleFavorite = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
if (isFavorite) {
|
||||
FavoriteService.removeFromFavorites(series.id);
|
||||
} else {
|
||||
FavoriteService.addToFavorites(series.id);
|
||||
const response = await fetch("/api/komga/favorites", {
|
||||
method: isFavorite ? "DELETE" : "POST",
|
||||
headers: {
|
||||
"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);
|
||||
if (onSeriesUpdate) {
|
||||
onSeriesUpdate({ ...series, favorite: !isFavorite });
|
||||
}
|
||||
|
||||
// Dispatch l'événement pour notifier les autres composants
|
||||
window.dispatchEvent(new Event("favoritesChanged"));
|
||||
|
||||
toast({
|
||||
title: isFavorite ? "Retiré des favoris" : "Ajouté aux favoris",
|
||||
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 { storageService } from "./storage.service";
|
||||
|
||||
interface AuthUser {
|
||||
id: string;
|
||||
@@ -7,19 +8,6 @@ interface AuthUser {
|
||||
roles: string[];
|
||||
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 {
|
||||
private static instance: AuthService;
|
||||
|
||||
@@ -36,38 +24,65 @@ class AuthService {
|
||||
* Authentifie un utilisateur
|
||||
*/
|
||||
async login(email: string, password: string, remember: boolean = false): Promise<void> {
|
||||
// En développement, on vérifie juste l'utilisateur de démo
|
||||
if (email === DEV_USER.email && password === DEV_USER.password) {
|
||||
storageService.setUserData(DEV_USER.userData, remember);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ email, password, remember }),
|
||||
});
|
||||
|
||||
throw {
|
||||
code: "INVALID_CREDENTIALS",
|
||||
message: "Email ou mot de passe incorrect",
|
||||
} as AuthError;
|
||||
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 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
|
||||
*/
|
||||
logout(): void {
|
||||
storageService.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>();
|
||||
async logout(): Promise<void> {
|
||||
await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { AuthConfig } from "@/types/auth";
|
||||
import { serverCacheService } from "./server-cache.service";
|
||||
import { komgaConfigService } from "./komga-config.service";
|
||||
import { ConfigDBService } from "./config-db.service";
|
||||
|
||||
// Types de cache disponibles
|
||||
export type CacheType = "DEFAULT" | "HOME" | "LIBRARIES" | "SERIES" | "BOOKS" | "IMAGES";
|
||||
|
||||
export abstract class BaseApiService {
|
||||
protected static async getKomgaConfig(): Promise<AuthConfig> {
|
||||
const cookiesStore = cookies();
|
||||
return komgaConfigService.validateAndGetConfig(cookiesStore);
|
||||
try {
|
||||
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 {
|
||||
|
||||
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 {
|
||||
private static readonly FAVORITES_CHANGE_EVENT = "favoritesChanged";
|
||||
|
||||
private static dispatchFavoritesChanged() {
|
||||
// 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
|
||||
*/
|
||||
static isFavorite(seriesId: string): boolean {
|
||||
return storageService.isFavorite(seriesId);
|
||||
static async isFavorite(seriesId: string): Promise<boolean> {
|
||||
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
|
||||
*/
|
||||
static addToFavorites(seriesId: string): void {
|
||||
storageService.addFavorite(seriesId);
|
||||
this.dispatchFavoritesChanged();
|
||||
static async addToFavorites(seriesId: string): Promise<void> {
|
||||
try {
|
||||
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
|
||||
*/
|
||||
static removeFromFavorites(seriesId: string): void {
|
||||
storageService.removeFavorite(seriesId);
|
||||
this.dispatchFavoritesChanged();
|
||||
static async removeFromFavorites(seriesId: string): Promise<void> {
|
||||
try {
|
||||
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
|
||||
*/
|
||||
static getAllFavoriteIds(): string[] {
|
||||
return storageService.getFavorites();
|
||||
static async getAllFavoriteIds(): Promise<string[]> {
|
||||
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 type { NextRequest } from "next/server";
|
||||
import { komgaConfigService } from "@/lib/services/komga-config.service";
|
||||
|
||||
// Routes qui ne nécessitent pas d'authentification
|
||||
const publicRoutes = ["/login", "/register", "/images"];
|
||||
@@ -15,29 +14,18 @@ export function middleware(request: NextRequest) {
|
||||
if (
|
||||
publicRoutes.includes(pathname) ||
|
||||
publicApiRoutes.includes(pathname) ||
|
||||
pathname.startsWith("/images/")
|
||||
pathname.startsWith("/images/") ||
|
||||
pathname.startsWith("/_next/")
|
||||
) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// Vérifier si c'est une route d'API
|
||||
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
|
||||
// Pour toutes les routes protégées, vérifier la présence de l'utilisateur
|
||||
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);
|
||||
loginUrl.searchParams.set("from", pathname);
|
||||
return NextResponse.redirect(loginUrl);
|
||||
@@ -45,10 +33,14 @@ export function middleware(request: NextRequest) {
|
||||
|
||||
try {
|
||||
const userData = JSON.parse(atob(user.value));
|
||||
if (!userData.authenticated) {
|
||||
throw new Error("User not authenticated");
|
||||
if (!userData || !userData.authenticated || !userData.id || !userData.email) {
|
||||
throw new Error("Invalid user data");
|
||||
}
|
||||
} 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);
|
||||
loginUrl.searchParams.set("from", pathname);
|
||||
return NextResponse.redirect(loginUrl);
|
||||
@@ -66,7 +58,7 @@ export const config = {
|
||||
* 2. /_next/* (Next.js internals)
|
||||
* 3. /fonts/* (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).*)",
|
||||
],
|
||||
|
||||
@@ -19,11 +19,4 @@ export interface AuthError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type AuthErrorCode =
|
||||
| "INVALID_CREDENTIALS"
|
||||
| "INVALID_SERVER_URL"
|
||||
| "SERVER_UNREACHABLE"
|
||||
| "NETWORK_ERROR"
|
||||
| "UNKNOWN_ERROR"
|
||||
| "CACHE_CLEAR_ERROR"
|
||||
| "TEST_CONNECTION_ERROR";
|
||||
export type AuthErrorCode = "INVALID_CREDENTIALS" | "SERVER_ERROR" | "EMAIL_EXISTS";
|
||||
|
||||
Reference in New Issue
Block a user