From 1f881ade26e45526903047339a5cb0dcdec413e1 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Fri, 14 Feb 2025 14:23:30 +0100 Subject: [PATCH] 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; + } +}