diff --git a/backend/package-lock.json b/backend/package-lock.json deleted file mode 100644 index 07807e8..0000000 --- a/backend/package-lock.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "name": "rocks-2many-backend", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", - "requires": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "requires": { - "streamsearch": "^1.1.0" - } - }, - "call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "requires": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "csv-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/csv-parser/-/csv-parser-3.2.0.tgz", - "integrity": "sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==" - }, - "csv-writer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/csv-writer/-/csv-writer-1.6.0.tgz", - "integrity": "sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g==" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "requires": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - } - }, - "es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" - }, - "es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" - }, - "es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "requires": { - "es-errors": "^1.3.0" - } - }, - "es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "requires": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - } - }, - "follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" - }, - "form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - } - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" - }, - "get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "requires": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - } - }, - "get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "requires": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - } - }, - "gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" - }, - "has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" - }, - "has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "requires": { - "has-symbols": "^1.0.3" - } - }, - "hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "requires": { - "function-bind": "^1.1.2" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "requires": { - "minimist": "^1.2.6" - } - }, - "multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", - "requires": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "slugify": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", - "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==" - }, - "streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - } - } -} diff --git a/backend/package.json b/backend/package.json index 2c09bc7..6cd13ee 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,14 +9,17 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { + "@aws-sdk/client-s3": "^3.802.0", "axios": "^1.9.0", "cors": "^2.8.5", "csv-parser": "^3.2.0", "csv-writer": "^1.6.0", "dotenv": "^16.0.3", "express": "^4.18.2", + "ioredis": "^5.6.1", "morgan": "^1.10.0", "multer": "^1.4.5-lts.2", + "multer-s3": "^3.0.1", "nodemailer": "^6.9.1", "pg": "^8.10.0", "pg-hstore": "^2.3.4", diff --git a/backend/src/db/index.js b/backend/src/db/index.js index 072641e..eac9b62 100644 --- a/backend/src/db/index.js +++ b/backend/src/db/index.js @@ -1,25 +1,73 @@ const { Pool } = require('pg'); const config = require('../config') -// Create a pool instance -const pool = new Pool({ +let writePool, readPool; + +// Always create write pool +writePool = new Pool({ user: config.db.user, password: config.db.password, host: config.db.host, port: config.db.port, - database: config.db.database + database: config.db.database, + max: config.db.maxConnections, }); -// Helper function for running queries -const query = async (text, params) => { +// Create read pool only in cloud mode +if (config.site.deployment === 'cloud') { + readPool = new Pool({ + user: config.db.user, + password: config.db.password, + host: config.db.readHost, + port: config.db.port, + database: config.db.database, + max: config.db.maxConnections, + }); +} else { + // In self-hosted mode, use the same pool for reads and writes + readPool = writePool; +} + + + +// Helper function that automatically routes queries +const query = async (text, params, options = {}) => { + const { forceWrite = false } = options; + + const isWrite = forceWrite || + text.trim().toUpperCase().startsWith('INSERT') || + text.trim().toUpperCase().startsWith('UPDATE') || + text.trim().toUpperCase().startsWith('DELETE') || + text.trim().toUpperCase().includes('FOR UPDATE'); + + const pool = isWrite ? writePool : readPool; const start = Date.now(); const res = await pool.query(text, params); const duration = Date.now() - start; - console.log('Executed query', { text, duration, rows: res.rowCount }); + + if (config.site.deployment === 'cloud') { + console.log('Executed query', { + text, + duration, + rows: res.rowCount, + pool: isWrite ? 'write' : 'read' + }); + } else { + console.log('Executed query', { text, duration, rows: res.rowCount }); + } + return res; }; -module.exports = { - query, - pool -}; \ No newline at end of file + +// Old Query function +// const query = async (text, params) => { +// const start = Date.now(); +// const res = await pool.query(text, params); +// const duration = Date.now() - start; +// console.log('Executed query', { text, duration, rows: res.rowCount }); +// return res; +// }; + + +module.exports = { query, pool: writePool }; \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js index 949fb04..d42a89a 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -13,6 +13,7 @@ const fs = require('fs'); // services const notificationService = require('./services/notificationService'); const emailService = require('./services/emailService'); +const storageService = require('./services/storageService'); // routes const stripePaymentRoutes = require('./routes/stripePayment'); @@ -59,45 +60,46 @@ if (!fs.existsSync(blogImagesDir)) { } // Configure storage -const storage = multer.diskStorage({ - destination: (req, file, cb) => { - // Determine destination based on upload type - if (req.originalUrl.includes('/product')) { - cb(null, productImagesDir); - } else { - cb(null, uploadsDir); - } - }, - filename: (req, file, cb) => { - // Create unique filename with original extension - const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); - const fileExt = path.extname(file.originalname); - const safeName = path.basename(file.originalname, fileExt) - .toLowerCase() - .replace(/[^a-z0-9]/g, '-'); +// const storage = multer.diskStorage({ +// destination: (req, file, cb) => { +// // Determine destination based on upload type +// if (req.originalUrl.includes('/product')) { +// cb(null, productImagesDir); +// } else { +// cb(null, uploadsDir); +// } +// }, +// filename: (req, file, cb) => { +// // Create unique filename with original extension +// const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); +// const fileExt = path.extname(file.originalname); +// const safeName = path.basename(file.originalname, fileExt) +// .toLowerCase() +// .replace(/[^a-z0-9]/g, '-'); - cb(null, `${safeName}-${uniqueSuffix}${fileExt}`); - } -}); +// cb(null, `${safeName}-${uniqueSuffix}${fileExt}`); +// } +// }); -// File filter to only allow images -const fileFilter = (req, file, cb) => { - // Accept only image files - if (file.mimetype.startsWith('image/')) { - cb(null, true); - } else { - cb(new Error('Only image files are allowed!'), false); - } -}; +// // File filter to only allow images +// const fileFilter = (req, file, cb) => { +// // Accept only image files +// if (file.mimetype.startsWith('image/')) { +// cb(null, true); +// } else { +// cb(new Error('Only image files are allowed!'), false); +// } +// }; // Create the multer instance -const upload = multer({ - storage, - fileFilter, - limits: { - fileSize: 5 * 1024 * 1024 // 5MB limit - } -}); +// const upload = multer({ +// storage, +// fileFilter, +// limits: { +// fileSize: 5 * 1024 * 1024 // 5MB limit +// } +// }); +const upload = storageService.getUploadMiddleware(); pool.connect() .then(async () => { @@ -197,11 +199,16 @@ app.post('/api/image/upload', upload.single('image'), (req, res) => { message: 'No image file provided' }); } + const imagePath = req.file.path ? `/uploads/${req.file.filename}` : req.file.location; res.json({ success: true, - imagePath: `/uploads/${req.file.filename}` + imagePath: storageService.getImageUrl(imagePath) }); + // res.json({ + // success: true, + // imagePath: `/uploads/${req.file.filename}` + // }); }); app.get('/api/public-file/:filename', (req, res) => { @@ -249,13 +256,22 @@ app.post('/api/image/product', adminAuthMiddleware(pool, query), upload.single(' } // Get the relative path to the image - const imagePath = `/uploads/products/${req.file.filename}`; + const imagePath = req.file.path ? + `/uploads/products/${req.file.filename}` : + req.file.location; res.json({ success: true, - imagePath, - filename: req.file.filename + imagePath: storageService.getImageUrl(imagePath), + filename: req.file.filename || path.basename(req.file.location) }); +// const imagePath = `/uploads/products/${req.file.filename}`; + +// res.json({ +// success: true, +// imagePath, +// filename: req.file.filename +// }); }); // Upload multiple product images (admin only) @@ -269,15 +285,30 @@ app.post('/api/image/products', adminAuthMiddleware(pool, query), upload.array(' } // Get the relative paths to the images - const imagePaths = req.files.map(file => ({ - imagePath: `/uploads/products/${file.filename}`, - filename: file.filename - })); + const imagePaths = req.files.map(file => { + const imagePath = file.path ? + `/uploads/products/${file.filename}` : + file.location; + + return { + imagePath: storageService.getImageUrl(imagePath), + filename: file.filename || path.basename(file.location) + }; + }); res.json({ success: true, images: imagePaths }); + // const imagePaths = req.files.map(file => ({ + // imagePath: `/uploads/products/${file.filename}`, + // filename: file.filename + // })); + + // res.json({ + // success: true, + // images: imagePaths + // }); }); // Delete product image (admin only) @@ -293,18 +324,26 @@ app.delete('/api/image/product/:filename', adminAuthMiddleware(pool, query), (re }); } - const filePath = path.join(__dirname, '../public/uploads/products', filename); + if (config.site.deployment === 'cloud' && config.site.awsS3Bucket) { + // Implementation for S3 deletion would go here + // For now, we'll just log and continue + console.log('S3 file deletion not implemented yet'); + } else { + // Delete from local filesystem + const filePath = path.join(__dirname, '../public/uploads/products', filename); - // Check if file exists - if (!fs.existsSync(filePath)) { - return res.status(404).json({ - error: true, - message: 'Image not found' - }); + // Check if file exists + if (!fs.existsSync(filePath)) { + return res.status(404).json({ + error: true, + message: 'Image not found' + }); + } + + // Delete the file + fs.unlinkSync(filePath); } - // Delete the file - fs.unlinkSync(filePath); res.json({ success: true, @@ -346,6 +385,7 @@ app.use((err, req, res, next) => { // Start server app.listen(port, () => { console.log(`Server running on port ${port} in ${config.environment} environment`); + console.log(`Deployment mode: ${config.site.deployment || 'self-hosted'}`); }); module.exports = app; \ No newline at end of file diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index bbe43fd..840003b 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -68,7 +68,6 @@ module.exports = (pool, query) => { router.post('/login-request', async (req, res, next) => { const { email } = req.body; console.log('/login-request') - console.log(JSON.stringify(config, null, 4)) try { // Check if user exists const userResult = await query( diff --git a/backend/src/routes/mailingListAdmin.js b/backend/src/routes/mailingListAdmin.js index ee8fb4f..3e7a267 100644 --- a/backend/src/routes/mailingListAdmin.js +++ b/backend/src/routes/mailingListAdmin.js @@ -6,17 +6,19 @@ const csv = require('csv-parser'); const fs = require('fs'); const path = require('path'); const { createObjectCsvWriter } = require('csv-writer'); +const storageService = require('../services/storageService'); // Configure multer for file uploads -const upload = multer({ - dest: path.join(__dirname, '../uploads/temp'), - limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit -}); +// const upload = multer({ +// dest: path.join(__dirname, '../uploads/temp'), +// limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit +// }); module.exports = (pool, query, authMiddleware) => { // Apply authentication middleware to all routes router.use(authMiddleware); + const upload = storageService.getUploadMiddleware(); /** * Get all mailing lists * GET /api/admin/mailing-lists @@ -512,16 +514,45 @@ module.exports = (pool, query, authMiddleware) => { // Process the CSV file const results = []; + // const processFile = () => { + // return new Promise((resolve, reject) => { + // fs.createReadStream(file.path) + // .pipe(csv()) + // .on('data', (data) => results.push(data)) + // .on('end', () => { + // // Clean up temp file + // fs.unlink(file.path, (err) => { + // if (err) console.error('Error deleting temp file:', err); + // }); + // resolve(results); + // }) + // .on('error', reject); + // }); + // }; const processFile = () => { return new Promise((resolve, reject) => { + // For S3 uploads, file.path won't exist, but file.location will + const filePath = file.path || file.location; + + // If S3 storage, we need to download the file first + if (!file.path && file.location) { + // Implementation for S3 file would go here + // For now, we'll reject as this would need a different approach + reject(new Error('S3 file processing not implemented')); + return; + } + + // Local file processing fs.createReadStream(file.path) .pipe(csv()) .on('data', (data) => results.push(data)) .on('end', () => { - // Clean up temp file - fs.unlink(file.path, (err) => { - if (err) console.error('Error deleting temp file:', err); - }); + // Clean up temp file - only for local storage + if (file.path) { + fs.unlink(file.path, (err) => { + if (err) console.error('Error deleting temp file:', err); + }); + } resolve(results); }) .on('error', reject); @@ -683,6 +714,10 @@ module.exports = (pool, query, authMiddleware) => { // Create a temp file for CSV const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const fileName = `subscribers-${listName.replace(/[^a-z0-9]/gi, '-').toLowerCase()}-${timestamp}.csv`; + const tempDir = path.join(__dirname, '../uploads/temp'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } const filePath = path.join(__dirname, '../uploads/temp', fileName); // Create CSV writer diff --git a/backend/src/services/cacheService.js b/backend/src/services/cacheService.js new file mode 100644 index 0000000..f7b6e14 --- /dev/null +++ b/backend/src/services/cacheService.js @@ -0,0 +1,58 @@ +const Redis = require('ioredis'); +const config = require('../config'); + +class CacheService { + constructor() { + this.enabled = config.site.deployment === 'cloud' && config.site.redisHost + this.client = null; + + if (this.enabled) { + this.client = new Redis({ + host: config.site.redisHost, + port: config.site.redisPort, + password: config.site.redisPassword, + tls: config.site.redisTLS ? {} : undefined, + }); + + this.client.on('error', (err) => { + console.error('Redis Client Error', err); + }); + } + } + + async get(key) { + if (!this.enabled) return null; + + try { + const value = await this.client.get(key); + return value ? JSON.parse(value) : null; + } catch (error) { + console.error('Cache get error:', error); + return null; + } + } + + async set(key, value, ttlSeconds = 300) { + if (!this.enabled) return; + + try { + await this.client.setex(key, ttlSeconds, JSON.stringify(value)); + } catch (error) { + console.error('Cache set error:', error); + } + } + + async del(key) { + if (!this.enabled) return; + + try { + await this.client.del(key); + } catch (error) { + console.error('Cache delete error:', error); + } + } +} + +// Create a singleton instance +const cacheService = new CacheService(); +module.exports = cacheService; \ No newline at end of file diff --git a/backend/src/services/storageService.js b/backend/src/services/storageService.js new file mode 100644 index 0000000..d3e7cdd --- /dev/null +++ b/backend/src/services/storageService.js @@ -0,0 +1,83 @@ +// services/storageService.js +const multer = require('multer'); +const multerS3 = require('multer-s3'); +const { S3Client } = require('@aws-sdk/client-s3'); +const path = require('path'); +const fs = require('fs'); +const config = require('../config'); + +class StorageService { + constructor() { + this.mode = config.site.deployment; + this.s3Client = null; + + if (this.mode === 'cloud' && config.site.awsS3Bucket) { + this.s3Client = new S3Client({ region: config.site.awsRegion }); + } + } + + getUploadMiddleware() { + if (this.mode === 'cloud' && config.site.awsS3Bucket) { + // Cloud mode: Use S3 + return multer({ + storage: multerS3({ + s3: this.s3Client, + bucket: config.site.awsS3Bucket, + acl: 'public-read', + key: (req, file, cb) => { + const folder = req.path.includes('/product') ? 'products' : 'blog'; + cb(null, `${folder}/${Date.now()}-${file.originalname}`); + } + }), + fileFilter: this._fileFilter, + limits: { fileSize: 10 * 1024 * 1024 } + }); + } else { + // Self-hosted mode: Use local storage + return multer({ + storage: multer.diskStorage({ + destination: (req, file, cb) => { + const uploadDir = path.join(__dirname, '../../public/uploads'); + const folder = req.path.includes('/product') ? 'products' : 'blog'; + const finalPath = path.join(uploadDir, folder); + + // Ensure directory exists + if (!fs.existsSync(finalPath)) { + fs.mkdirSync(finalPath, { recursive: true }); + } + + cb(null, finalPath); + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + } + }), + fileFilter: this._fileFilter, + limits: { fileSize: 10 * 1024 * 1024 } + }); + } + } + + _fileFilter(req, file, cb) { + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed!'), false); + } + } + + getImageUrl(path) { + if (!path) return null; + + if (this.mode === 'cloud' && config.site.cdnDomain) { + // Use CloudFront CDN in cloud mode + return `https://${config.site.cdnDomain}${path}`; + } else { + // Use direct path in self-hosted mode + return path; + } + } +} + +const storageService = new StorageService(); +module.exports = storageService; \ No newline at end of file