From 1f881ade26e45526903047339a5cb0dcdec413e1 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 14:23:30 +0100 Subject: [PATCH 01/12] feat(db): init mongo and passing komga conf --- .env | 7 + .env.example | 7 + docker-compose.yml | 28 ++ package-lock.json | 360 ++++++++++++++++- package.json | 3 + src/app/api/komga/config/route.ts | 55 ++- src/app/settings/page.tsx | 447 +-------------------- src/components/settings/ClientSettings.tsx | 438 ++++++++++++++++++++ src/lib/models/config.model.ts | 35 ++ src/lib/mongodb.ts | 51 +++ src/lib/services/config-db.service.ts | 62 +++ 11 files changed, 1047 insertions(+), 446 deletions(-) create mode 100644 .env create mode 100644 .env.example create mode 100644 src/components/settings/ClientSettings.tsx create mode 100644 src/lib/models/config.model.ts create mode 100644 src/lib/mongodb.ts create mode 100644 src/lib/services/config-db.service.ts diff --git a/.env b/.env new file mode 100644 index 0000000..aeed6a5 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +# MongoDB +MONGO_USER=admin +MONGO_PASSWORD=password +MONGODB_URI=mongodb://admin:password@localhost:27017/paniels?authSource=admin + +# Komga +NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aeed6a5 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# MongoDB +MONGO_USER=admin +MONGO_PASSWORD=password +MONGODB_URI=mongodb://admin:password@localhost:27017/paniels?authSource=admin + +# Komga +NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7eee5ee..ace774a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,3 +16,31 @@ services: - NODE_ENV=development - NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com command: npm run dev + + mongodb: + image: mongo:latest + container_name: paniels_mongodb + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + + mongo-express: + image: mongo-express:latest + container_name: paniels_mongo_express + restart: always + ports: + - "8081:8081" + environment: + ME_CONFIG_MONGODB_ADMINUSERNAME: ${MONGO_USER} + ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGO_PASSWORD} + ME_CONFIG_MONGODB_URL: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongodb:27017/ + depends_on: + - mongodb + +volumes: + mongodb_data: diff --git a/package-lock.json b/package-lock.json index f25d241..854eaa5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,15 +12,17 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-slot": "^1.1.2", "@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 +54,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 +667,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 +880,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", @@ -1516,6 +1545,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 +1598,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 +2288,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 +2531,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 +2637,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 +4537,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 +4614,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 +4744,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 +4803,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 +4989,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 +5083,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 +5218,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 +5236,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 +5600,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 +5630,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 +5651,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 +5821,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 +6220,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 +6256,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 +6265,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 +6689,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 +6972,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 +7214,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", diff --git a/package.json b/package.json index 1d6aae0..f80a281 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,13 @@ "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-slot": "^1.1.2", "@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", diff --git a/src/app/api/komga/config/route.ts b/src/app/api/komga/config/route.ts index 6a78b62..c1e3a2c 100644 --- a/src/app/api/komga/config/route.ts +++ b/src/app/api/komga/config/route.ts @@ -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 } ); } } diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 2c9e166..2a9b0d4 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,429 +1,24 @@ -"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; -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(null); - const [success, setSuccess] = useState(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) => { - 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) => { - const { name, value } = event.target; - setConfig((prev) => ({ - ...prev, - [name]: value, - })); - }; - - const handleTTLChange = (event: React.ChangeEvent) => { - const { name, value } = event.target; - setTTLConfig((prev) => ({ - ...prev, - [name]: parseInt(value || "0", 10), - })); - }; - - const handleSaveTTL = (event: React.FormEvent) => { - 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 ( -
-
-

Préférences

-
- - {/* Messages de succès/erreur */} - {error && ( -
-

{error.message}

-
- )} - {success && ( -
-

{success}

-
- )} - -
- {/* Section Configuration Komga */} -
-
-
-

- - Configuration Komga -

-

- Configurez les informations de connexion à votre serveur Komga. -

-
- - {/* Formulaire de configuration */} -
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
-
- - {/* Section Configuration du Cache */} -
-
-
-

- - Configuration du Cache -

-

- Gérez les paramètres de mise en cache des données. -

-
- - {/* Formulaire TTL */} -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
-
-
-
- ); + try { + const mongoConfig = await ConfigDBService.getConfig(); + // Convertir le document Mongoose en objet simple + if (mongoConfig) { + config = { + url: mongoConfig.url, + username: mongoConfig.username, + password: mongoConfig.password, + userId: mongoConfig.userId, + }; + } + } 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 ; } diff --git a/src/components/settings/ClientSettings.tsx b/src/components/settings/ClientSettings.tsx new file mode 100644 index 0000000..cd19686 --- /dev/null +++ b/src/components/settings/ClientSettings.tsx @@ -0,0 +1,438 @@ +"use client"; + +import { useState, useEffect } 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"; + +interface ErrorMessage { + message: string; +} + +interface KomgaConfig { + url: string; + username: string; + password: string; + userId: string; +} + +interface ClientSettingsProps { + initialConfig: KomgaConfig | null; +} + +export function ClientSettings({ initialConfig }: ClientSettingsProps) { + console.log("initialConfig", initialConfig); + const router = useRouter(); + const { toast } = useToast(); + const [isLoading, setIsLoading] = useState(false); + const [isCacheClearing, setIsCacheClearing] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [config, setConfig] = useState({ + serverUrl: initialConfig?.url || "", + username: initialConfig?.username || "", + password: initialConfig?.password || "", + }); + const [ttlConfig, setTTLConfig] = useState({ + defaultTTL: 5, + homeTTL: 5, + librariesTTL: 1440, + seriesTTL: 5, + booksTTL: 5, + imagesTTL: 1440, + }); + + useEffect(() => { + // 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) => { + 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", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + url: newConfig.serverUrl, + 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", + }); + }; + + const handleInputChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + setConfig((prev) => ({ + ...prev, + [name]: value, + })); + }; + + const handleTTLChange = (event: React.ChangeEvent) => { + const { name, value } = event.target; + setTTLConfig((prev) => ({ + ...prev, + [name]: parseInt(value || "0", 10), + })); + }; + + const handleSaveTTL = (event: React.FormEvent) => { + 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 ( +
+
+

Préférences

+
+ + {/* Messages de succès/erreur */} + {error && ( +
+

{error.message}

+
+ )} + {success && ( +
+

{success}

+
+ )} + +
+ {/* Section Configuration Komga */} +
+
+
+

+ + Configuration Komga +

+

+ Configurez les informations de connexion à votre serveur Komga. +

+
+ + {/* Formulaire de configuration */} +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ + {/* Section Configuration du Cache */} +
+
+
+

+ + Configuration du Cache +

+

+ Gérez les paramètres de mise en cache des données. +

+
+ + {/* Formulaire TTL */} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+
+ ); +} diff --git a/src/lib/models/config.model.ts b/src/lib/models/config.model.ts new file mode 100644 index 0000000..04ace86 --- /dev/null +++ b/src/lib/models/config.model.ts @@ -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); diff --git a/src/lib/mongodb.ts b/src/lib/mongodb.ts new file mode 100644 index 0000000..af5f061 --- /dev/null +++ b/src/lib/mongodb.ts @@ -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 | 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 { + 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; diff --git a/src/lib/services/config-db.service.ts b/src/lib/services/config-db.service.ts new file mode 100644 index 0000000..a35b137 --- /dev/null +++ b/src/lib/services/config-db.service.ts @@ -0,0 +1,62 @@ +import { cookies } from "next/headers"; +import connectDB from "@/lib/mongodb"; +import { KomgaConfig } from "@/lib/models/config.model"; + +interface User { + id: string; + email: string; +} + +interface KomgaConfigData { + url: string; + username: string; + password: string; +} + +export class ConfigDBService { + private static async getCurrentUser(): Promise { + 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; + } +} From 6b3866e54df7dd0da6bfeb80bf593275881d2d64 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 14:35:40 +0100 Subject: [PATCH 02/12] refacto(db): get config from mongo everywhere --- src/lib/services/base-api.service.ts | 17 ++++++++++++++--- src/middleware.ts | 27 ++++++++++----------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/lib/services/base-api.service.ts b/src/lib/services/base-api.service.ts index 8716901..ec60c82 100644 --- a/src/lib/services/base-api.service.ts +++ b/src/lib/services/base-api.service.ts @@ -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 { - 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 { diff --git a/src/middleware.ts b/src/middleware.ts index 1c651e8..4cc6cff 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -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"]; @@ -20,24 +19,12 @@ export function middleware(request: NextRequest) { 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 (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); @@ -46,9 +33,15 @@ export function middleware(request: NextRequest) { try { const userData = JSON.parse(atob(user.value)); if (!userData.authenticated) { + if (pathname.startsWith("/api/")) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } throw new Error("User not authenticated"); } } catch (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); From 33717c485dad0fd23c32d9c416045577e5efe5e8 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 14:39:43 +0100 Subject: [PATCH 03/12] refacto(db): remove old setconfig --- src/components/settings/ClientSettings.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/settings/ClientSettings.tsx b/src/components/settings/ClientSettings.tsx index cd19686..1dc6bd1 100644 --- a/src/components/settings/ClientSettings.tsx +++ b/src/components/settings/ClientSettings.tsx @@ -6,7 +6,6 @@ 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"; interface ErrorMessage { message: string; @@ -147,8 +146,6 @@ export function ClientSettings({ initialConfig }: ClientSettingsProps) { }, }; - komgaConfigService.setConfig(komgaConfig, true); - fetch("/api/komga/config", { method: "POST", headers: { From 349f71969f8bf59933942119f726a6737dafeb4c Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 14:42:45 +0100 Subject: [PATCH 04/12] refacto(db): removing komgaconfig service --- src/app/libraries/[libraryId]/page.tsx | 2 - src/app/page.tsx | 2 - src/app/series/[seriesId]/page.tsx | 3 - src/lib/services/komga-config.service.ts | 134 ----------------------- 4 files changed, 141 deletions(-) delete mode 100644 src/lib/services/komga-config.service.ts diff --git a/src/app/libraries/[libraryId]/page.tsx b/src/app/libraries/[libraryId]/page.tsx index e60abf4..6fdb6b5 100644 --- a/src/app/libraries/[libraryId]/page.tsx +++ b/src/app/libraries/[libraryId]/page.tsx @@ -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 { diff --git a/src/app/page.tsx b/src/app/page.tsx index 7617450..8464672 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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() { diff --git a/src/app/series/[seriesId]/page.tsx b/src/app/series/[seriesId]/page.tsx index 0278288..e82f597 100644 --- a/src/app/series/[seriesId]/page.tsx +++ b/src/app/series/[seriesId]/page.tsx @@ -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 }; diff --git a/src/lib/services/komga-config.service.ts b/src/lib/services/komga-config.service.ts deleted file mode 100644 index e5a7cdc..0000000 --- a/src/lib/services/komga-config.service.ts +++ /dev/null @@ -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(); From 518913645e99e3c9f178d73631481118e4f4f746 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 14:53:39 +0100 Subject: [PATCH 05/12] refactor(db): better docker --- .dockerignore | 1 - .env | 2 +- .env.example | 2 +- docker-compose.yml | 17 ++--------------- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/.dockerignore b/.dockerignore index 1c1b2e9..d28a3ed 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,6 @@ .gitignore node_modules .next -.env .env.local .env.development.local .env.test.local diff --git a/.env b/.env index aeed6a5..9275886 100644 --- a/.env +++ b/.env @@ -1,7 +1,7 @@ # MongoDB MONGO_USER=admin MONGO_PASSWORD=password -MONGODB_URI=mongodb://admin:password@localhost:27017/paniels?authSource=admin +MONGODB_URI=mongodb://admin:password@localhost:27017/stripstream?authSource=admin # Komga NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com \ No newline at end of file diff --git a/.env.example b/.env.example index aeed6a5..9275886 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ # MongoDB MONGO_USER=admin MONGO_PASSWORD=password -MONGODB_URI=mongodb://admin:password@localhost:27017/paniels?authSource=admin +MONGODB_URI=mongodb://admin:password@localhost:27017/stripstream?authSource=admin # Komga NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ace774a..616d034 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,12 +14,12 @@ 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: paniels_mongodb + container_name: stripstream_mongodb restart: always environment: MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} @@ -29,18 +29,5 @@ services: volumes: - mongodb_data:/data/db - mongo-express: - image: mongo-express:latest - container_name: paniels_mongo_express - restart: always - ports: - - "8081:8081" - environment: - ME_CONFIG_MONGODB_ADMINUSERNAME: ${MONGO_USER} - ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGO_PASSWORD} - ME_CONFIG_MONGODB_URL: mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongodb:27017/ - depends_on: - - mongodb - volumes: mongodb_data: From b71ccd6b0e7f732ce3b15494ec31b88bf1e9a251 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 15:19:08 +0100 Subject: [PATCH 06/12] feat(db): loading and review save conf Komga --- src/components/settings/ClientSettings.tsx | 84 ++++++++++++++-------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/src/components/settings/ClientSettings.tsx b/src/components/settings/ClientSettings.tsx index 1dc6bd1..fb07180 100644 --- a/src/components/settings/ClientSettings.tsx +++ b/src/components/settings/ClientSettings.tsx @@ -27,6 +27,7 @@ export function ClientSettings({ initialConfig }: 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(null); const [success, setSuccess] = useState(null); @@ -123,9 +124,11 @@ export function ClientSettings({ initialConfig }: ClientSettingsProps) { } }; - const handleSave = (event: React.FormEvent) => { + const handleSave = async (event: React.FormEvent) => { event.preventDefault(); setSuccess(null); + setError(null); + setIsSaving(true); const formData = new FormData(event.currentTarget); const serverUrl = formData.get("serverUrl") as string; @@ -138,36 +141,53 @@ export function ClientSettings({ initialConfig }: ClientSettingsProps) { password, }; - const komgaConfig = { - serverUrl: newConfig.serverUrl, - credentials: { - username: newConfig.username, - password: newConfig.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, + }), + }); - 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"); + } - setConfig(newConfig); + const komgaConfig = { + serverUrl: newConfig.serverUrl, + credentials: { + username: newConfig.username, + password: newConfig.password, + }, + }; - // Émettre un événement pour notifier les autres composants - const configChangeEvent = new CustomEvent("komga-config-changed", { detail: komgaConfig }); - window.dispatchEvent(configChangeEvent); + setConfig(newConfig); - toast({ - title: "Configuration sauvegardée", - description: "La configuration a été sauvegardée avec succès", - }); + // É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) => { @@ -278,9 +298,17 @@ export function ClientSettings({ initialConfig }: ClientSettingsProps) {
- -
- - - ); -} - -export default function LoginPage() { - return ( - Chargement...} - > - - - ); +export default function LoginPage({ + searchParams, +}: { + searchParams: { from?: string; tab?: string }; +}) { + return ; } diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..a4c347c --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,102 @@ +"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(null); + + const handleSubmit = async (event: React.FormEvent) => { + 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 || "/"); + } catch (error) { + setError(error as AuthError); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ + +
+
+ + +
+
+ + +
+ {error && ( +
+ {error.message} +
+ )} + +
+ ); +} diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx new file mode 100644 index 0000000..4b05fba --- /dev/null +++ b/src/components/auth/RegisterForm.tsx @@ -0,0 +1,110 @@ +"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(null); + + const handleSubmit = async (event: React.FormEvent) => { + 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 || "/"); + } catch (error) { + setError(error as AuthError); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ + +
+
+ + +
+
+ + +
+ {error && ( +
+ {error.message} +
+ )} + +
+ ); +} diff --git a/src/components/layout/SidebarWrapper.tsx b/src/components/layout/SidebarWrapper.tsx index 7a6415a..7f4cecb 100644 --- a/src/components/layout/SidebarWrapper.tsx +++ b/src/components/layout/SidebarWrapper.tsx @@ -1,6 +1,5 @@ import { FavoriteService } from "@/lib/services/favorite.service"; import { LibraryService } from "@/lib/services/library.service"; -import { ClientSidebar } from "./ClientSidebar"; export async function SidebarWrapper() { // Récupérer les favoris depuis le serveur diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..9651cac --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/lib/models/user.model.ts b/src/lib/models/user.model.ts new file mode 100644 index 0000000..2c3fa35 --- /dev/null +++ b/src/lib/models/user.model.ts @@ -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); diff --git a/src/lib/services/auth.service.ts b/src/lib/services/auth.service.ts index f331255..01f8c1e 100644 --- a/src/lib/services/auth.service.ts +++ b/src/lib/services/auth.service.ts @@ -1,3 +1,5 @@ +"use client"; + import { AuthError } from "@/types/auth"; import { storageService } from "./storage.service"; @@ -7,19 +9,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,23 +25,79 @@ class AuthService { * Authentifie un utilisateur */ async login(email: string, password: string, remember: boolean = false): Promise { - // 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; + } + + const data = await response.json(); + if (data.user) { + storageService.setUserData(data.user, remember); + } + } 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 { + 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; + } + + const data = await response.json(); + if (data.user) { + storageService.setUserData(data.user, false); + } + } 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(); + async logout(): Promise { + try { + await fetch("/api/auth/logout", { + method: "POST", + }); + } finally { + storageService.clear(); + } } /** diff --git a/src/middleware.ts b/src/middleware.ts index 4cc6cff..4b8653f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -14,14 +14,15 @@ export function middleware(request: NextRequest) { if ( publicRoutes.includes(pathname) || publicApiRoutes.includes(pathname) || - pathname.startsWith("/images/") + pathname.startsWith("/images/") || + pathname.startsWith("/_next/") ) { return NextResponse.next(); } // 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 }); } @@ -32,13 +33,11 @@ export function middleware(request: NextRequest) { try { const userData = JSON.parse(atob(user.value)); - if (!userData.authenticated) { - if (pathname.startsWith("/api/")) { - return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); - } - 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 }); } @@ -59,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).*)", ], diff --git a/src/types/auth.ts b/src/types/auth.ts index f332763..7397e6e 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -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"; From ca36f4ce6a2505d10c7887a42b17fae38986f941 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 17:07:44 +0100 Subject: [PATCH 09/12] refacto(db): TTL conf in mongo --- src/app/api/komga/ttl-config/route.ts | 47 ++++++++++++++ src/app/settings/page.tsx | 8 ++- src/components/settings/ClientSettings.tsx | 75 ++++++++++++++-------- src/lib/models/ttl-config.model.ts | 46 +++++++++++++ src/lib/services/config-db.service.ts | 54 ++++++++++++++++ 5 files changed, 203 insertions(+), 27 deletions(-) create mode 100644 src/app/api/komga/ttl-config/route.ts create mode 100644 src/lib/models/ttl-config.model.ts diff --git a/src/app/api/komga/ttl-config/route.ts b/src/app/api/komga/ttl-config/route.ts new file mode 100644 index 0000000..2b0923d --- /dev/null +++ b/src/app/api/komga/ttl-config/route.ts @@ -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 } + ); + } +} diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 2a9b0d4..ca930ca 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -3,10 +3,11 @@ import { ClientSettings } from "@/components/settings/ClientSettings"; export default async function SettingsPage() { let config = null; + let ttlConfig = null; try { + // Récupérer la configuration Komga const mongoConfig = await ConfigDBService.getConfig(); - // Convertir le document Mongoose en objet simple if (mongoConfig) { config = { url: mongoConfig.url, @@ -15,10 +16,13 @@ export default async function SettingsPage() { 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 ; + return ; } diff --git a/src/components/settings/ClientSettings.tsx b/src/components/settings/ClientSettings.tsx index fb07180..c2ec58e 100644 --- a/src/components/settings/ClientSettings.tsx +++ b/src/components/settings/ClientSettings.tsx @@ -18,12 +18,21 @@ interface KomgaConfig { userId: string; } -interface ClientSettingsProps { - initialConfig: KomgaConfig | null; +interface TTLConfigData { + defaultTTL: number; + homeTTL: number; + librariesTTL: number; + seriesTTL: number; + booksTTL: number; + imagesTTL: number; } -export function ClientSettings({ initialConfig }: ClientSettingsProps) { - console.log("initialConfig", initialConfig); +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); @@ -36,22 +45,16 @@ export function ClientSettings({ initialConfig }: ClientSettingsProps) { username: initialConfig?.username || "", password: initialConfig?.password || "", }); - const [ttlConfig, setTTLConfig] = useState({ - defaultTTL: 5, - homeTTL: 5, - librariesTTL: 1440, - seriesTTL: 5, - booksTTL: 5, - imagesTTL: 1440, - }); - - useEffect(() => { - // Charger la configuration des TTL - const savedTTLConfig = storageService.getTTLConfig(); - if (savedTTLConfig) { - setTTLConfig(savedTTLConfig); + const [ttlConfig, setTTLConfig] = useState( + initialTTLConfig || { + defaultTTL: 5, + homeTTL: 5, + librariesTTL: 1440, + seriesTTL: 5, + booksTTL: 5, + imagesTTL: 1440, } - }, []); + ); const handleClearCache = async () => { setIsCacheClearing(true); @@ -206,15 +209,37 @@ export function ClientSettings({ initialConfig }: ClientSettingsProps) { })); }; - const handleSaveTTL = (event: React.FormEvent) => { + const handleSaveTTL = async (event: React.FormEvent) => { 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", - }); + 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 ( diff --git a/src/lib/models/ttl-config.model.ts b/src/lib/models/ttl-config.model.ts new file mode 100644 index 0000000..a95d947 --- /dev/null +++ b/src/lib/models/ttl-config.model.ts @@ -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); diff --git a/src/lib/services/config-db.service.ts b/src/lib/services/config-db.service.ts index a35b137..cb66977 100644 --- a/src/lib/services/config-db.service.ts +++ b/src/lib/services/config-db.service.ts @@ -1,6 +1,7 @@ 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; @@ -13,6 +14,15 @@ interface KomgaConfigData { password: string; } +interface TTLConfigData { + defaultTTL: number; + homeTTL: number; + librariesTTL: number; + seriesTTL: number; + booksTTL: number; + imagesTTL: number; +} + export class ConfigDBService { private static async getCurrentUser(): Promise { const userCookie = cookies().get("stripUser"); @@ -59,4 +69,48 @@ export class ConfigDBService { 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, + }; + } } From 85eeae0a1bfe39c19d2f49e967537b4217861a26 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 17:19:15 +0100 Subject: [PATCH 10/12] refacto(db): auth and user cleaner --- src/app/api/auth/login/route.ts | 55 +++++---------- src/app/api/auth/register/route.ts | 63 +++++------------ src/lib/services/auth-server.service.ts | 90 +++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 82 deletions(-) create mode 100644 src/lib/services/auth-server.service.ts diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 759a88d..2e8c6b5 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,48 +1,29 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import connectDB from "@/lib/mongodb"; -import { UserModel } from "@/lib/models/user.model"; +import { AuthServerService } from "@/lib/services/auth-server.service"; export async function POST(request: Request) { try { - const { email, password, remember } = await request.json(); - await connectDB(); + const { email, password } = await request.json(); - const user = await UserModel.findOne({ email: email.toLowerCase() }); + try { + const userData = await AuthServerService.loginUser(email, password); + AuthServerService.setUserCookie(userData); - if (!user || user.password !== password) { - return NextResponse.json( - { - error: { - code: "INVALID_CREDENTIALS", - message: "Email ou mot de passe incorrect", + 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 } - ); + { status: 401 } + ); + } + throw error; } - - const userData = { - id: user._id.toString(), - email: user.email, - roles: user.roles, - authenticated: true, - }; - - // Encoder les données utilisateur en base64 - const encodedUserData = Buffer.from(JSON.stringify(userData)).toString("base64"); - - // Définir le cookie avec les données utilisateur - cookies().set("stripUser", encodedUserData, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - path: "/", - // 30 jours si "remember me" est coché, sinon 24 heures - maxAge: remember ? 30 * 24 * 60 * 60 : 24 * 60 * 60, - }); - - return NextResponse.json({ message: "Connexion réussie", user: userData }); } catch (error) { console.error("Erreur lors de la connexion:", error); return NextResponse.json( diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 9561b13..f317ec8 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -1,56 +1,29 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import connectDB from "@/lib/mongodb"; -import { UserModel } from "@/lib/models/user.model"; +import { AuthServerService } from "@/lib/services/auth-server.service"; export async function POST(request: Request) { try { const { email, password } = await request.json(); - await connectDB(); - // Vérifier si l'utilisateur existe déjà - const existingUser = await UserModel.findOne({ email: email.toLowerCase() }); - if (existingUser) { - return NextResponse.json( - { - error: { - code: "EMAIL_EXISTS", - message: "Cet email est déjà utilisé", + 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 } - ); + { status: 400 } + ); + } + throw error; } - - // Créer le nouvel utilisateur - const user = await UserModel.create({ - email: email.toLowerCase(), - password, - roles: ["ROLE_USER"], - authenticated: true, - }); - - const userData = { - id: user._id.toString(), - email: user.email, - roles: user.roles, - authenticated: true, - }; - - // Encoder les données utilisateur en base64 - const encodedUserData = Buffer.from(JSON.stringify(userData)).toString("base64"); - - // Définir le cookie avec les données utilisateur - cookies().set("stripUser", encodedUserData, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "lax", - path: "/", - // 24 heures par défaut pour les nouveaux utilisateurs - maxAge: 24 * 60 * 60, - }); - - return NextResponse.json({ message: "Inscription réussie", user: userData }); } catch (error) { console.error("Erreur lors de l'inscription:", error); return NextResponse.json( diff --git a/src/lib/services/auth-server.service.ts b/src/lib/services/auth-server.service.ts new file mode 100644 index 0000000..185dde0 --- /dev/null +++ b/src/lib/services/auth-server.service.ts @@ -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 { + 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 { + 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; + } +} From d7bed4df6d1feffd788d9a2782e5d590185b80ff Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 21:40:04 +0100 Subject: [PATCH 11/12] refacto(db): remove localstorage service --- src/components/layout/ClientLayout.tsx | 10 - src/components/layout/Sidebar.tsx | 2 - src/components/settings/ClientSettings.tsx | 3 +- src/lib/services/auth.service.ts | 36 +--- src/lib/services/storage.service.ts | 221 --------------------- 5 files changed, 4 insertions(+), 268 deletions(-) delete mode 100644 src/lib/services/storage.service.ts diff --git a/src/components/layout/ClientLayout.tsx b/src/components/layout/ClientLayout.tsx index 593db05..bfca57f 100644 --- a/src/components/layout/ClientLayout.tsx +++ b/src/components/layout/ClientLayout.tsx @@ -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); }; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 21145a2..dd7b3a5 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -7,7 +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"; interface SidebarProps { isOpen: boolean; @@ -99,7 +98,6 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) { const handleLogout = () => { authService.logout(); - storageService.clearAll(); setLibraries([]); setFavorites([]); onClose(); diff --git a/src/components/settings/ClientSettings.tsx b/src/components/settings/ClientSettings.tsx index c2ec58e..5160464 100644 --- a/src/components/settings/ClientSettings.tsx +++ b/src/components/settings/ClientSettings.tsx @@ -1,9 +1,8 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState } 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"; diff --git a/src/lib/services/auth.service.ts b/src/lib/services/auth.service.ts index 01f8c1e..ab8ccee 100644 --- a/src/lib/services/auth.service.ts +++ b/src/lib/services/auth.service.ts @@ -1,7 +1,6 @@ "use client"; import { AuthError } from "@/types/auth"; -import { storageService } from "./storage.service"; interface AuthUser { id: string; @@ -38,11 +37,6 @@ class AuthService { const data = await response.json(); throw data.error; } - - const data = await response.json(); - if (data.user) { - storageService.setUserData(data.user, remember); - } } catch (error) { if ((error as AuthError).code) { throw error; @@ -71,11 +65,6 @@ class AuthService { const data = await response.json(); throw data.error; } - - const data = await response.json(); - if (data.user) { - storageService.setUserData(data.user, false); - } } catch (error) { if ((error as AuthError).code) { throw error; @@ -91,28 +80,9 @@ class AuthService { * Déconnecte l'utilisateur */ async logout(): Promise { - try { - await fetch("/api/auth/logout", { - method: "POST", - }); - } finally { - storageService.clear(); - } - } - - /** - * Vérifie si l'utilisateur est connecté - */ - isAuthenticated(): boolean { - const user = storageService.getUserData(); - return !!user?.authenticated; - } - - /** - * Récupère l'utilisateur connecté - */ - getCurrentUser(): AuthUser | null { - return storageService.getUserData(); + await fetch("/api/auth/logout", { + method: "POST", + }); } } diff --git a/src/lib/services/storage.service.ts b/src/lib/services/storage.service.ts deleted file mode 100644 index 2c59615..0000000 --- a/src/lib/services/storage.service.ts +++ /dev/null @@ -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(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 | 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(); From d6939820b98933d7de6fc0ab41ddce0af1b2e6f4 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 22:06:15 +0100 Subject: [PATCH 12/12] fix(db); no more bug on switch connection --- src/components/auth/LoginForm.tsx | 1 + src/components/auth/RegisterForm.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx index a4c347c..21218fe 100644 --- a/src/components/auth/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -27,6 +27,7 @@ export function LoginForm({ from }: LoginFormProps) { try { await authService.login(email, password, remember); router.push(from || "/"); + router.refresh(); } catch (error) { setError(error as AuthError); } finally { diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index 4b05fba..c5c4e43 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -36,6 +36,7 @@ export function RegisterForm({ from }: RegisterFormProps) { try { await authService.register(email, password); router.push(from || "/"); + router.refresh(); } catch (error) { setError(error as AuthError); } finally {