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 new file mode 100644 index 0000000..68c89d3 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +# MongoDB +MONGO_USER=admin +MONGO_PASSWORD=password +MONGODB_URI=mongodb://admin:password@mongodb.paniels.orb.local:27017/stripstream?authSource=admin + +# Komga +NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9275886 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# MongoDB +MONGO_USER=admin +MONGO_PASSWORD=password +MONGODB_URI=mongodb://admin:password@localhost:27017/stripstream?authSource=admin + +# Komga +NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7eee5ee..616d034 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,5 +14,20 @@ services: - /app/.next environment: - NODE_ENV=development - - NEXT_PUBLIC_API_URL=https://cloud.julienfroidefond.com + - NEXT_PUBLIC_API_URL= ${NEXT_PUBLIC_API_URL} command: npm run dev + + mongodb: + image: mongo:latest + container_name: stripstream_mongodb + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + +volumes: + mongodb_data: diff --git a/package-lock.json b/package-lock.json index f25d241..2fe36ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,19 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.6", + "@types/mongoose": "^5.11.97", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.323.0", + "mongoose": "^8.10.0", "next": "^14.1.0", + "next-auth": "^4.24.11", "next-themes": "^0.4.4", "react": "^18.2.0", "react-dom": "^18.2.0", "sharp": "^0.33.2", - "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.22.4" @@ -52,6 +55,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", @@ -654,6 +668,14 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.2.0.tgz", + "integrity": "sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@next/env": { "version": "14.2.23", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz", @@ -859,6 +881,14 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1310,6 +1340,35 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.6.tgz", @@ -1516,6 +1575,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mongoose": { + "version": "5.11.97", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz", + "integrity": "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==", + "deprecated": "Mongoose publishes its own types, so you do not need to install this package.", + "dependencies": { + "mongoose": "*" + } + }, "node_modules/@types/node": { "version": "20.17.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.17.tgz", @@ -1560,6 +1628,19 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", @@ -2237,6 +2318,14 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.2.tgz", + "integrity": "sha512-5afhLTjqDSA3akH56E+/2J6kTDuSIlBxyXPdQslj9hcIgOUE378xdOfZvC/9q3LifJNI6KR/juZ+d0NRNYBwXg==", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2472,6 +2561,14 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2570,7 +2667,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4471,6 +4567,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4540,6 +4644,14 @@ "node": ">=4.0" } }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4662,6 +4774,11 @@ "node": ">= 0.4" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4716,11 +4833,104 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mongodb": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz", + "integrity": "sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.9", + "bson": "^6.10.1", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.10.0.tgz", + "integrity": "sha512-nLhk3Qrv6q/HpD2k1O7kbBqsq+/kmKpdv5KJ+LLhQlII3e1p/SSLoLP6jMuSiU6+iLK7zFw4T1niAk3mA3QVug==", + "dependencies": { + "bson": "^6.10.1", + "kareem": "2.6.3", + "mongodb": "~6.13.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -4809,6 +5019,37 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.11", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14 || ^15", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next-themes": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz", @@ -4872,6 +5113,11 @@ "node": ">=0.10.0" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5002,6 +5248,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", + "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5012,6 +5266,39 @@ "wrappy": "1" } }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5343,6 +5630,26 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/preact": { + "version": "10.25.4", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.4.tgz", + "integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5353,6 +5660,11 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5369,7 +5681,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5540,6 +5851,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -5934,6 +6250,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -5965,15 +6286,6 @@ "node": ">=8" } }, - "node_modules/sonner": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", - "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5983,6 +6295,14 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", @@ -6399,6 +6719,17 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -6671,6 +7002,34 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.1.tgz", + "integrity": "sha512-mDGf9diDad/giZ/Sm9Xi2YcyzaFpbdLpJPr+E9fSkyQ7KpQD4SdFcugkRQYzhmfI4KeV4Qpnn2sKPdo+kmsgRQ==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6885,6 +7244,11 @@ "dev": true, "license": "ISC" }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/yaml": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", diff --git a/package.json b/package.json index 1d6aae0..dfdb2d5 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,15 @@ "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-toast": "^1.2.6", + "@types/mongoose": "^5.11.97", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.323.0", + "mongoose": "^8.10.0", "next": "^14.1.0", + "next-auth": "^4.24.11", "next-themes": "^0.4.4", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..2e8c6b5 --- /dev/null +++ b/src/app/api/auth/login/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { AuthServerService } from "@/lib/services/auth-server.service"; + +export async function POST(request: Request) { + try { + const { email, password } = await request.json(); + + try { + const userData = await AuthServerService.loginUser(email, password); + AuthServerService.setUserCookie(userData); + + return NextResponse.json({ message: "Connexion réussie", user: userData }); + } catch (error) { + if (error instanceof Error && error.message === "INVALID_CREDENTIALS") { + return NextResponse.json( + { + error: { + code: "INVALID_CREDENTIALS", + message: "Email ou mot de passe incorrect", + }, + }, + { status: 401 } + ); + } + throw error; + } + } catch (error) { + console.error("Erreur lors de la connexion:", error); + return NextResponse.json( + { + error: { + code: "SERVER_ERROR", + message: "Une erreur est survenue lors de la connexion", + }, + }, + { status: 500 } + ); + } +} diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts new file mode 100644 index 0000000..8eee72d --- /dev/null +++ b/src/app/api/auth/logout/route.ts @@ -0,0 +1,9 @@ +import { NextResponse } from "next/server"; +import { cookies } from "next/headers"; + +export async function POST() { + // Supprimer le cookie + cookies().delete("stripUser"); + + return NextResponse.json({ message: "Déconnexion réussie" }); +} diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..f317ec8 --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { AuthServerService } from "@/lib/services/auth-server.service"; + +export async function POST(request: Request) { + try { + const { email, password } = await request.json(); + + try { + const userData = await AuthServerService.createUser(email, password); + AuthServerService.setUserCookie(userData); + + return NextResponse.json({ message: "Inscription réussie", user: userData }); + } catch (error) { + if (error instanceof Error && error.message === "EMAIL_EXISTS") { + return NextResponse.json( + { + error: { + code: "EMAIL_EXISTS", + message: "Cet email est déjà utilisé", + }, + }, + { status: 400 } + ); + } + throw error; + } + } catch (error) { + console.error("Erreur lors de l'inscription:", error); + return NextResponse.json( + { + error: { + code: "SERVER_ERROR", + message: "Une erreur est survenue lors de l'inscription", + }, + }, + { status: 500 } + ); + } +} 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/api/komga/favorites/route.ts b/src/app/api/komga/favorites/route.ts new file mode 100644 index 0000000..6299109 --- /dev/null +++ b/src/app/api/komga/favorites/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import { FavoriteService } from "@/lib/services/favorite.service"; + +export async function GET() { + try { + const favoriteIds = await FavoriteService.getAllFavoriteIds(); + return NextResponse.json(favoriteIds); + } catch (error) { + console.error("Erreur lors de la récupération des favoris:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des favoris" }, + { status: 500 } + ); + } +} + +export async function POST(request: Request) { + try { + const { seriesId } = await request.json(); + await FavoriteService.addToFavorites(seriesId); + return NextResponse.json({ message: "Favori ajouté avec succès" }); + } catch (error) { + console.error("Erreur lors de l'ajout du favori:", error); + return NextResponse.json({ error: "Erreur lors de l'ajout du favori" }, { status: 500 }); + } +} + +export async function DELETE(request: Request) { + try { + const { seriesId } = await request.json(); + await FavoriteService.removeFromFavorites(seriesId); + return NextResponse.json({ message: "Favori supprimé avec succès" }); + } catch (error) { + console.error("Erreur lors de la suppression du favori:", error); + return NextResponse.json({ error: "Erreur lors de la suppression du favori" }, { status: 500 }); + } +} 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/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/login/LoginContent.tsx b/src/app/login/LoginContent.tsx new file mode 100644 index 0000000..2de8157 --- /dev/null +++ b/src/app/login/LoginContent.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { LoginForm } from "@/components/auth/LoginForm"; +import { RegisterForm } from "@/components/auth/RegisterForm"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; + +interface LoginContentProps { + searchParams: { + from?: string; + tab?: string; + }; +} + +export function LoginContent({ searchParams }: LoginContentProps) { + const defaultTab = searchParams.tab || "login"; + + return ( +
+
+
+
+
+ + + + StripStream +
+
+
+

+ Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne + et fluide. +

+
+
+
+
+
+
+

Bienvenue sur StripStream

+

+ Connectez-vous ou créez un compte pour commencer +

+
+ + + Connexion + Inscription + + + + + + + + +
+
+
+ ); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 4e5abfe..5b1d583 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,187 +1,15 @@ -"use client"; +import { Metadata } from "next"; +import { LoginContent } from "./LoginContent"; -import { useState, Suspense } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { authService } from "@/lib/services/auth.service"; -import { AuthError } from "@/types/auth"; +export const metadata: Metadata = { + title: "Connexion", + description: "Connectez-vous à votre compte StripStream", +}; -function LoginForm() { - const router = useRouter(); - const searchParams = useSearchParams(); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(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); - const from = searchParams.get("from") || "/"; - router.push(from); - } catch (error) { - setError(error as AuthError); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-
-
-
- - - - Stripstream -
-
-
-

- Profitez de vos BD, mangas et comics préférés avec une expérience de lecture moderne - et fluide. -

-
-
-
-
-
-
-
-
-
- - - -
-
-
-

- Stripstream -

-

Votre bibliothèque numérique de BD

-
-
-
-
- -
-
- -
-
-
-

Connexion

-

- Connectez-vous pour accéder à votre bibliothèque -

-
-
-
- - -
-
- - -
-
- - -
- {error && ( -
- {error.message} -
- )} - -
-
-
-
- ); -} - -export default function LoginPage() { - return ( - Chargement...
} - > - - - ); +export default function LoginPage({ + searchParams, +}: { + searchParams: { from?: string; tab?: string }; +}) { + return ; } 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/app/settings/page.tsx b/src/app/settings/page.tsx index 2c9e166..ca930ca 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,429 +1,28 @@ -"use client"; +import { ConfigDBService } from "@/lib/services/config-db.service"; +import { ClientSettings } from "@/components/settings/ClientSettings"; -import { useState, useEffect, useRef } from "react"; -import { Loader2, Network, Trash2 } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { storageService } from "@/lib/services/storage.service"; -import { AuthError } from "@/types/auth"; -import { useToast } from "@/components/ui/use-toast"; -import { komgaConfigService } from "@/lib/services/komga-config.service"; +export default async function SettingsPage() { + let config = null; + let ttlConfig = null; -interface ErrorMessage { - message: string; -} - -export default function SettingsPage() { - const router = useRouter(); - const { toast } = useToast(); - const [isLoading, setIsLoading] = useState(false); - const [isCacheClearing, setIsCacheClearing] = useState(false); - const [error, setError] = useState(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 { + // Récupérer la configuration Komga + const mongoConfig = await ConfigDBService.getConfig(); + if (mongoConfig) { + config = { + url: mongoConfig.url, + username: mongoConfig.username, + password: mongoConfig.password, + userId: mongoConfig.userId, + }; + } + + // Récupérer la configuration TTL + ttlConfig = await ConfigDBService.getTTLConfig(); + } catch (error) { + console.error("Erreur lors de la récupération de la configuration:", error); + // On ne fait rien si la config n'existe pas, on laissera le composant client gérer l'état initial + } + + return ; } diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..21218fe --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { authService } from "@/lib/services/auth.service"; +import { AuthError } from "@/types/auth"; + +interface LoginFormProps { + from?: string; +} + +export function LoginForm({ from }: LoginFormProps) { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(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 || "/"); + router.refresh(); + } 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..c5c4e43 --- /dev/null +++ b/src/components/auth/RegisterForm.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { authService } from "@/lib/services/auth.service"; +import { AuthError } from "@/types/auth"; + +interface RegisterFormProps { + from?: string; +} + +export function RegisterForm({ from }: RegisterFormProps) { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(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 || "/"); + router.refresh(); + } catch (error) { + setError(error as AuthError); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ + +
+
+ + +
+
+ + +
+ {error && ( +
+ {error.message} +
+ )} + +
+ ); +} diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx index 899ebfe..0c89f3f 100644 --- a/src/components/home/HeroSection.tsx +++ b/src/components/home/HeroSection.tsx @@ -11,10 +11,10 @@ interface HeroSectionProps { } export function HeroSection({ series }: HeroSectionProps) { - console.log("HeroSection - Séries reçues:", { - count: series?.length || 0, - firstSeries: series?.[0], - }); + // console.log("HeroSection - Séries reçues:", { + // count: series?.length || 0, + // firstSeries: series?.[0], + // }); return (
diff --git a/src/components/home/HomeContent.tsx b/src/components/home/HomeContent.tsx index 63cced1..8519c2d 100644 --- a/src/components/home/HomeContent.tsx +++ b/src/components/home/HomeContent.tsx @@ -26,11 +26,11 @@ export function HomeContent({ data }: HomeContentProps) { }; // Vérification des données pour le debug - console.log("HomeContent - Données reçues:", { - ongoingCount: data.ongoing?.length || 0, - recentlyReadCount: data.recentlyRead?.length || 0, - onDeckCount: data.onDeck?.length || 0, - }); + // console.log("HomeContent - Données reçues:", { + // ongoingCount: data.ongoing?.length || 0, + // recentlyReadCount: data.recentlyRead?.length || 0, + // onDeckCount: data.onDeck?.length || 0, + // }); return (
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 598c013..dd7b3a5 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,3 +1,5 @@ +"use client"; + import { BookOpen, Home, Library, Settings, LogOut, RefreshCw, Star } from "lucide-react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; @@ -5,8 +7,6 @@ import { cn } from "@/lib/utils"; import { authService } from "@/lib/services/auth.service"; import { useEffect, useState, useCallback } from "react"; import { KomgaLibrary, KomgaSeries } from "@/types/komga"; -import { storageService } from "@/lib/services/storage.service"; -import { FavoriteService } from "@/lib/services/favorite.service"; interface SidebarProps { isOpen: boolean; @@ -43,13 +43,20 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) { const fetchFavorites = useCallback(async () => { setIsLoadingFavorites(true); try { - const favoriteIds = FavoriteService.getAllFavoriteIds(); + // Récupérer les IDs des favoris depuis l'API + const favoritesResponse = await fetch("/api/komga/favorites"); + if (!favoritesResponse.ok) { + throw new Error("Erreur lors de la récupération des favoris"); + } + const favoriteIds = await favoritesResponse.json(); + if (favoriteIds.length === 0) { setFavorites([]); return; } - const promises = favoriteIds.map(async (id) => { + // Récupérer les détails des séries pour chaque ID + const promises = favoriteIds.map(async (id: string) => { const response = await fetch(`/api/komga/series/${id}`); if (!response.ok) return null; return response.json(); @@ -69,7 +76,7 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) { useEffect(() => { fetchLibraries(); fetchFavorites(); - }, []); // Suppression de la dépendance pathname + }, [fetchLibraries, fetchFavorites]); // Mettre à jour les favoris quand ils changent useEffect(() => { @@ -77,18 +84,10 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) { fetchFavorites(); }; - // Écouter les changements de favoris dans la même fenêtre window.addEventListener("favoritesChanged", handleFavoritesChange); - // Écouter les changements de favoris dans d'autres fenêtres - window.addEventListener("storage", (e) => { - if (e.key === "stripstream_favorites") { - fetchFavorites(); - } - }); return () => { window.removeEventListener("favoritesChanged", handleFavoritesChange); - window.removeEventListener("storage", handleFavoritesChange); }; }, [fetchFavorites]); @@ -99,7 +98,6 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) { const handleLogout = () => { authService.logout(); - storageService.clearAll(); setLibraries([]); setFavorites([]); onClose(); diff --git a/src/components/layout/SidebarWrapper.tsx b/src/components/layout/SidebarWrapper.tsx new file mode 100644 index 0000000..7f4cecb --- /dev/null +++ b/src/components/layout/SidebarWrapper.tsx @@ -0,0 +1,25 @@ +import { FavoriteService } from "@/lib/services/favorite.service"; +import { LibraryService } from "@/lib/services/library.service"; + +export async function SidebarWrapper() { + // Récupérer les favoris depuis le serveur + const favoriteIds = await FavoriteService.getAllFavoriteIds(); + + // Récupérer les détails des séries favorites + const favoritesPromises = favoriteIds.map(async (id) => { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/series/${id}`, { + headers: { + Accept: "application/json", + }, + }); + if (!response.ok) return null; + return response.json(); + }); + + // Récupérer les bibliothèques + const libraries = await LibraryService.getLibraries(); + + const favorites = (await Promise.all(favoritesPromises)).filter(Boolean); + + return { favorites, libraries }; +} diff --git a/src/components/series/SeriesHeader.tsx b/src/components/series/SeriesHeader.tsx index 85c05d8..b104966 100644 --- a/src/components/series/SeriesHeader.tsx +++ b/src/components/series/SeriesHeader.tsx @@ -1,11 +1,9 @@ "use client"; import Image from "next/image"; -import { ImageOff, Book, BookOpen, BookMarked } from "lucide-react"; +import { ImageOff, Book, BookOpen, BookMarked, Star, StarOff } from "lucide-react"; import { KomgaSeries } from "@/types/komga"; import { useState, useEffect } from "react"; -import { FavoriteService } from "@/lib/services/favorite.service"; -import { Star, StarOff } from "lucide-react"; import { Button } from "../ui/button"; import { useToast } from "@/components/ui/use-toast"; import { cn } from "@/lib/utils"; @@ -56,13 +54,27 @@ export const SeriesHeader = ({ series, onSeriesUpdate }: SeriesHeaderProps) => { const [readingStatus, setReadingStatus] = useState( getReadingStatusInfo(series) ); - const [isFavorite, setIsFavorite] = useState(FavoriteService.isFavorite(series.id)); + const [isFavorite, setIsFavorite] = useState(false); const [isLoading, setIsLoading] = useState(false); const [mounted, setMounted] = useState(false); + // Vérifier si la série est dans les favoris au chargement useEffect(() => { + const checkFavorite = async () => { + try { + const response = await fetch("/api/komga/favorites"); + if (response.ok) { + const favoriteIds = await response.json(); + setIsFavorite(favoriteIds.includes(series.id)); + } + } catch (error) { + console.error("Erreur lors de la vérification des favoris:", error); + } + }; + + checkFavorite(); setMounted(true); - }, []); + }, [series.id]); useEffect(() => { setReadingStatus(getReadingStatusInfo(series)); @@ -82,18 +94,29 @@ export const SeriesHeader = ({ series, onSeriesUpdate }: SeriesHeaderProps) => { } }, [series.metadata.language]); - const handleToggleFavorite = () => { + const handleToggleFavorite = async () => { try { setIsLoading(true); - if (isFavorite) { - FavoriteService.removeFromFavorites(series.id); - } else { - FavoriteService.addToFavorites(series.id); + const response = await fetch("/api/komga/favorites", { + method: isFavorite ? "DELETE" : "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ seriesId: series.id }), + }); + + if (!response.ok) { + throw new Error("Erreur lors de la modification des favoris"); } + setIsFavorite(!isFavorite); if (onSeriesUpdate) { onSeriesUpdate({ ...series, favorite: !isFavorite }); } + + // Dispatch l'événement pour notifier les autres composants + window.dispatchEvent(new Event("favoritesChanged")); + toast({ title: isFavorite ? "Retiré des favoris" : "Ajouté aux favoris", variant: "default", diff --git a/src/components/settings/ClientSettings.tsx b/src/components/settings/ClientSettings.tsx new file mode 100644 index 0000000..5160464 --- /dev/null +++ b/src/components/settings/ClientSettings.tsx @@ -0,0 +1,487 @@ +"use client"; + +import { useState } from "react"; +import { Loader2, Network, Trash2 } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { AuthError } from "@/types/auth"; +import { useToast } from "@/components/ui/use-toast"; + +interface ErrorMessage { + message: string; +} + +interface KomgaConfig { + url: string; + username: string; + password: string; + userId: string; +} + +interface TTLConfigData { + defaultTTL: number; + homeTTL: number; + librariesTTL: number; + seriesTTL: number; + booksTTL: number; + imagesTTL: number; +} + +interface ClientSettingsProps { + initialConfig: KomgaConfig | null; + initialTTLConfig: TTLConfigData | null; +} + +export function ClientSettings({ initialConfig, initialTTLConfig }: ClientSettingsProps) { + const router = useRouter(); + const { toast } = useToast(); + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [isCacheClearing, setIsCacheClearing] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [config, setConfig] = useState({ + serverUrl: initialConfig?.url || "", + username: initialConfig?.username || "", + password: initialConfig?.password || "", + }); + const [ttlConfig, setTTLConfig] = useState( + initialTTLConfig || { + defaultTTL: 5, + homeTTL: 5, + librariesTTL: 1440, + seriesTTL: 5, + booksTTL: 5, + imagesTTL: 1440, + } + ); + + const handleClearCache = async () => { + setIsCacheClearing(true); + setError(null); + setSuccess(null); + + try { + const response = await fetch("/api/komga/cache/clear", { + method: "POST", + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Erreur lors de la suppression du cache"); + } + + toast({ + title: "Cache supprimé", + description: "Cache serveur supprimé avec succès", + }); + router.refresh(); + } catch (error) { + console.error("Erreur:", error); + toast({ + variant: "destructive", + title: "Erreur", + description: error instanceof Error ? error.message : "Une erreur est survenue", + }); + } finally { + setIsCacheClearing(false); + } + }; + + const handleTest = async () => { + setIsLoading(true); + setError(null); + setSuccess(null); + + try { + const response = await fetch("/api/komga/test", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + serverUrl: config.serverUrl, + username: config.username, + password: config.password, + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Erreur lors du test de connexion"); + } + + toast({ + title: "Connexion réussie", + description: "La connexion au serveur Komga a été établie avec succès", + }); + } catch (error) { + console.error("Erreur:", error); + toast({ + variant: "destructive", + title: "Erreur", + description: error instanceof Error ? error.message : "Une erreur est survenue", + }); + } finally { + setIsLoading(false); + } + }; + + const handleSave = async (event: React.FormEvent) => { + event.preventDefault(); + setSuccess(null); + setError(null); + setIsSaving(true); + + const formData = new FormData(event.currentTarget); + const serverUrl = formData.get("serverUrl") as string; + const username = formData.get("username") as string; + const password = formData.get("password") as string; + + const newConfig = { + serverUrl: serverUrl.trim(), + username, + password, + }; + + try { + const response = await fetch("/api/komga/config", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + url: newConfig.serverUrl, + username: newConfig.username, + password: newConfig.password, + }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Erreur lors de la sauvegarde de la configuration"); + } + + const komgaConfig = { + serverUrl: newConfig.serverUrl, + credentials: { + username: newConfig.username, + password: newConfig.password, + }, + }; + + setConfig(newConfig); + + // Émettre un événement pour notifier les autres composants + const configChangeEvent = new CustomEvent("komga-config-changed", { detail: komgaConfig }); + window.dispatchEvent(configChangeEvent); + + toast({ + title: "Configuration sauvegardée", + description: "La configuration a été sauvegardée avec succès", + }); + } catch (error) { + console.error("Erreur lors de la sauvegarde:", error); + toast({ + variant: "destructive", + title: "Erreur", + description: + error instanceof Error ? error.message : "Une erreur est survenue lors de la sauvegarde", + }); + } finally { + setIsSaving(false); + } + }; + + const handleInputChange = (event: React.ChangeEvent) => { + 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 = async (event: React.FormEvent) => { + event.preventDefault(); + setSuccess(null); + + try { + const response = await fetch("/api/komga/ttl-config", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(ttlConfig), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Erreur lors de la sauvegarde de la configuration TTL"); + } + + toast({ + title: "Configuration TTL sauvegardée", + description: "La configuration des TTL a été sauvegardée avec succès", + }); + } catch (error) { + console.error("Erreur lors de la sauvegarde:", error); + toast({ + variant: "destructive", + title: "Erreur", + description: + error instanceof Error ? error.message : "Une erreur est survenue lors de la sauvegarde", + }); + } + }; + + return ( +
+
+

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/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/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/models/favorite.model.ts b/src/lib/models/favorite.model.ts new file mode 100644 index 0000000..9e345e2 --- /dev/null +++ b/src/lib/models/favorite.model.ts @@ -0,0 +1,23 @@ +import mongoose from "mongoose"; + +const favoriteSchema = new mongoose.Schema( + { + userId: { + type: String, + required: true, + index: true, + }, + seriesId: { + type: String, + required: true, + }, + }, + { + timestamps: true, + } +); + +// Index composé pour s'assurer qu'un utilisateur ne peut pas avoir deux fois le même favori +favoriteSchema.index({ userId: 1, seriesId: 1 }, { unique: true }); + +export const FavoriteModel = mongoose.models.Favorite || mongoose.model("Favorite", favoriteSchema); 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/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/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/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; + } +} diff --git a/src/lib/services/auth.service.ts b/src/lib/services/auth.service.ts index f331255..ab8ccee 100644 --- a/src/lib/services/auth.service.ts +++ b/src/lib/services/auth.service.ts @@ -1,5 +1,6 @@ +"use client"; + import { AuthError } from "@/types/auth"; -import { storageService } from "./storage.service"; interface AuthUser { id: string; @@ -7,19 +8,6 @@ interface AuthUser { roles: string[]; authenticated: boolean; } - -// Utilisateur de développement -const DEV_USER = { - email: "demo@stripstream.local", - password: "demo123", - userData: { - id: "1", - email: "demo@stripstream.local", - roles: ["ROLE_USER"], - authenticated: true, - } as AuthUser, -}; - class AuthService { private static instance: AuthService; @@ -36,38 +24,65 @@ class AuthService { * Authentifie un utilisateur */ async login(email: string, password: string, remember: boolean = false): Promise { - // En développement, on vérifie juste l'utilisateur de démo - if (email === DEV_USER.email && password === DEV_USER.password) { - storageService.setUserData(DEV_USER.userData, remember); - return; - } + try { + const response = await fetch("/api/auth/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password, remember }), + }); - throw { - code: "INVALID_CREDENTIALS", - message: "Email ou mot de passe incorrect", - } as AuthError; + if (!response.ok) { + const data = await response.json(); + throw data.error; + } + } catch (error) { + if ((error as AuthError).code) { + throw error; + } + throw { + code: "SERVER_ERROR", + message: "Une erreur est survenue lors de la connexion", + } as AuthError; + } + } + + /** + * Crée un nouvel utilisateur + */ + async register(email: string, password: string): Promise { + try { + const response = await fetch("/api/auth/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + const data = await response.json(); + throw data.error; + } + } catch (error) { + if ((error as AuthError).code) { + throw error; + } + throw { + code: "SERVER_ERROR", + message: "Une erreur est survenue lors de l'inscription", + } as AuthError; + } } /** * Déconnecte l'utilisateur */ - logout(): void { - storageService.clear(); - } - - /** - * Vérifie si l'utilisateur est connecté - */ - isAuthenticated(): boolean { - const user = storageService.getUserData(); - return !!user?.authenticated; - } - - /** - * Récupère l'utilisateur connecté - */ - getCurrentUser(): AuthUser | null { - return storageService.getUserData(); + async logout(): Promise { + await fetch("/api/auth/logout", { + method: "POST", + }); } } 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/lib/services/config-db.service.ts b/src/lib/services/config-db.service.ts new file mode 100644 index 0000000..cb66977 --- /dev/null +++ b/src/lib/services/config-db.service.ts @@ -0,0 +1,116 @@ +import { cookies } from "next/headers"; +import connectDB from "@/lib/mongodb"; +import { KomgaConfig } from "@/lib/models/config.model"; +import { TTLConfig } from "@/lib/models/ttl-config.model"; + +interface User { + id: string; + email: string; +} + +interface KomgaConfigData { + url: string; + username: string; + password: string; +} + +interface TTLConfigData { + defaultTTL: number; + homeTTL: number; + librariesTTL: number; + seriesTTL: number; + booksTTL: number; + imagesTTL: number; +} + +export class ConfigDBService { + private static async getCurrentUser(): Promise { + const userCookie = cookies().get("stripUser"); + + if (!userCookie) { + throw new Error("Utilisateur non authentifié"); + } + + try { + return JSON.parse(atob(userCookie.value)); + } catch (error) { + console.error("Erreur lors de la récupération de l'utilisateur depuis le cookie:", error); + throw new Error("Utilisateur non authentifié"); + } + } + + static async saveConfig(data: KomgaConfigData) { + const user = await this.getCurrentUser(); + await connectDB(); + + const config = await KomgaConfig.findOneAndUpdate( + { userId: user.id }, + { + userId: user.id, + url: data.url, + username: data.username, + password: data.password, + }, + { upsert: true, new: true } + ); + + return config; + } + + static async getConfig() { + const user = await this.getCurrentUser(); + await connectDB(); + + const config = await KomgaConfig.findOne({ userId: user.id }); + + if (!config) { + throw new Error("Configuration non trouvée"); + } + + return config; + } + + static async saveTTLConfig(data: TTLConfigData) { + const user = await this.getCurrentUser(); + await connectDB(); + + const config = await TTLConfig.findOneAndUpdate( + { userId: user.id }, + { + userId: user.id, + ...data, + }, + { upsert: true, new: true } + ); + + return config; + } + + static async getTTLConfig() { + const user = await this.getCurrentUser(); + await connectDB(); + + const config = await TTLConfig.findOne({ userId: user.id }); + + if (!config) { + // Retourner la configuration par défaut si aucune configuration n'existe + return { + defaultTTL: 5, + homeTTL: 5, + librariesTTL: 1440, + seriesTTL: 5, + booksTTL: 5, + imagesTTL: 1440, + }; + } + + return { + defaultTTL: config.defaultTTL, + homeTTL: config.homeTTL, + librariesTTL: config.librariesTTL, + seriesTTL: config.seriesTTL, + booksTTL: config.booksTTL, + imagesTTL: config.imagesTTL, + }; + } +} diff --git a/src/lib/services/favorite.service.ts b/src/lib/services/favorite.service.ts index bb079f9..ae18c64 100644 --- a/src/lib/services/favorite.service.ts +++ b/src/lib/services/favorite.service.ts @@ -1,40 +1,111 @@ -import { storageService } from "./storage.service"; +import { cookies } from "next/headers"; +import connectDB from "@/lib/mongodb"; +import { FavoriteModel } from "@/lib/models/favorite.model"; + +interface User { + id: string; + email: string; +} export class FavoriteService { private static readonly FAVORITES_CHANGE_EVENT = "favoritesChanged"; private static dispatchFavoritesChanged() { // Dispatch l'événement pour notifier les changements - window.dispatchEvent(new Event(FavoriteService.FAVORITES_CHANGE_EVENT)); + if (typeof window !== "undefined") { + window.dispatchEvent(new Event(FavoriteService.FAVORITES_CHANGE_EVENT)); + } + } + + private static async getCurrentUser(): Promise { + const userCookie = cookies().get("stripUser"); + + if (!userCookie) { + throw new Error("Utilisateur non authentifié"); + } + + try { + return JSON.parse(atob(userCookie.value)); + } catch (error) { + console.error("Erreur lors de la récupération de l'utilisateur depuis le cookie:", error); + throw new Error("Utilisateur non authentifié"); + } } /** * Vérifie si une série est dans les favoris */ - static isFavorite(seriesId: string): boolean { - return storageService.isFavorite(seriesId); + static async isFavorite(seriesId: string): Promise { + try { + const user = await this.getCurrentUser(); + await connectDB(); + + const favorite = await FavoriteModel.findOne({ + userId: user.id, + seriesId: seriesId, + }); + + return !!favorite; + } catch (error) { + console.error("Erreur lors de la vérification du favori:", error); + return false; + } } /** * Ajoute une série aux favoris */ - static addToFavorites(seriesId: string): void { - storageService.addFavorite(seriesId); - this.dispatchFavoritesChanged(); + static async addToFavorites(seriesId: string): Promise { + try { + const user = await this.getCurrentUser(); + await connectDB(); + + await FavoriteModel.findOneAndUpdate( + { userId: user.id, seriesId }, + { userId: user.id, seriesId }, + { upsert: true } + ); + + this.dispatchFavoritesChanged(); + } catch (error) { + console.error("Erreur lors de l'ajout aux favoris:", error); + throw new Error("Erreur lors de l'ajout aux favoris"); + } } /** * Retire une série des favoris */ - static removeFromFavorites(seriesId: string): void { - storageService.removeFavorite(seriesId); - this.dispatchFavoritesChanged(); + static async removeFromFavorites(seriesId: string): Promise { + try { + const user = await this.getCurrentUser(); + await connectDB(); + + await FavoriteModel.findOneAndDelete({ + userId: user.id, + seriesId, + }); + + this.dispatchFavoritesChanged(); + } catch (error) { + console.error("Erreur lors de la suppression des favoris:", error); + throw new Error("Erreur lors de la suppression des favoris"); + } } /** * Récupère tous les IDs des séries favorites */ - static getAllFavoriteIds(): string[] { - return storageService.getFavorites(); + static async getAllFavoriteIds(): Promise { + try { + const user = await this.getCurrentUser(); + await connectDB(); + + const favorites = await FavoriteModel.find({ userId: user.id }); + return favorites.map((favorite) => favorite.seriesId); + } catch (error) { + console.error("Erreur lors de la récupération des favoris:", error); + return []; + } } } 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(); 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(); diff --git a/src/middleware.ts b/src/middleware.ts index 1c651e8..4b8653f 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"]; @@ -15,29 +14,18 @@ export function middleware(request: NextRequest) { if ( publicRoutes.includes(pathname) || publicApiRoutes.includes(pathname) || - pathname.startsWith("/images/") + pathname.startsWith("/images/") || + pathname.startsWith("/_next/") ) { return NextResponse.next(); } - // Vérifier si c'est une route d'API - if (pathname.startsWith("/api/")) { - // Vérifier la configuration Komga - const config = komgaConfigService.getConfig(request.cookies); - - if (!komgaConfigService.isConfigValid(config)) { - return NextResponse.json( - { error: "Configuration Komga manquante ou invalide" }, - { status: 401 } - ); - } - - return NextResponse.next(); - } - - // Pour les routes protégées, vérifier la présence de l'utilisateur + // Pour toutes les routes protégées, vérifier la présence de l'utilisateur const user = request.cookies.get("stripUser"); - if (!user) { + if (!user || !user.value) { + if (pathname.startsWith("/api/")) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("from", pathname); return NextResponse.redirect(loginUrl); @@ -45,10 +33,14 @@ export function middleware(request: NextRequest) { try { const userData = JSON.parse(atob(user.value)); - if (!userData.authenticated) { - throw new Error("User not authenticated"); + if (!userData || !userData.authenticated || !userData.id || !userData.email) { + throw new Error("Invalid user data"); } } catch (error) { + console.error("Erreur de validation du cookie:", error); + if (pathname.startsWith("/api/")) { + return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + } const loginUrl = new URL("/login", request.url); loginUrl.searchParams.set("from", pathname); return NextResponse.redirect(loginUrl); @@ -66,7 +58,7 @@ export const config = { * 2. /_next/* (Next.js internals) * 3. /fonts/* (inside public directory) * 4. /images/* (inside public directory) - * 5. /favicon.ico, /sitemap.xml (public files) + * 5. /favicon.ico, sitemap.xml (public files) */ "/((?!api/auth|_next/static|_next/image|fonts|images|favicon.ico|sitemap.xml).*)", ], 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";