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 (
+
+
+
+
+
+
+
+
+ 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 (
-
-
-
-
-
-
-
-
- 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
-
-
-
-
-
-
- );
-}
-
-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 && (
-
- )}
- {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 (
+
+ );
+}
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 (
+
+ );
+}
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 && (
+
+ )}
+ {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";